├── security ├── config │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── application-security.properties │ │ │ └── java │ │ │ └── com │ │ │ └── educational │ │ │ └── platform │ │ │ └── security │ │ │ ├── JwtTokenValidationException.java │ │ │ └── SecurityConfiguration.java │ └── build.gradle.kts └── test │ └── build.gradle.kts ├── docs ├── architecture_tests.png ├── bounded_context_map.png ├── integration_events.png ├── global_module_structure.png ├── business_submodules_structure.png ├── architecture-decisions │ ├── 0010-use-axon-framework.md │ ├── 0009-architecture-tests.md │ ├── 0008-result-from-comand-handlers.md │ ├── 0006-rich-domain-model.md │ ├── 0005-identifier-between-modules.md │ ├── 0011-use-axon-event-publishing-mechanism.md │ ├── 0012-make-application-framework-independent.md │ ├── 0003-cqrs.md │ ├── 0002-integration-events-implementation.md │ ├── 0004-validation.md │ ├── 0001-bounded-contexts-communication.md │ └── 0007-api-first.md └── bounded_context_map.puml ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── administration ├── web │ ├── src │ │ └── test │ │ │ ├── resources │ │ │ └── insert_data.sql │ │ │ └── java │ │ │ └── com │ │ │ └── educational │ │ │ └── platform │ │ │ └── TestApplication.java │ └── build.gradle.kts ├── application │ ├── src │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── educational │ │ │ │ └── platform │ │ │ │ ├── TestApplication.java │ │ │ │ └── administration │ │ │ │ └── course │ │ │ │ ├── jpa │ │ │ │ └── CourseProposalRepositoryTest.java │ │ │ │ └── create │ │ │ │ ├── SendCourseToApproveIntegrationEventHandlerTest.java │ │ │ │ ├── CreateCourseProposalCommandHandlerTest.java │ │ │ │ └── integration │ │ │ │ └── CreateCourseProposalCommandHandlerIntegrationTest.java │ │ └── main │ │ │ ├── java │ │ │ └── com │ │ │ │ └── educational │ │ │ │ └── platform │ │ │ │ └── administration │ │ │ │ └── course │ │ │ │ ├── query │ │ │ │ ├── ListCourseProposalsQuery.java │ │ │ │ └── ListCourseProposalsQueryHandler.java │ │ │ │ ├── approve │ │ │ │ └── ApproveCourseProposalCommand.java │ │ │ │ ├── create │ │ │ │ ├── CreateCourseProposalCommand.java │ │ │ │ ├── SendCourseToApproveIntegrationEventHandler.java │ │ │ │ └── CreateCourseProposalCommandHandler.java │ │ │ │ ├── decline │ │ │ │ └── DeclineCourseProposalCommand.java │ │ │ │ ├── CourseProposalStatusDTO.java │ │ │ │ ├── CourseProposalDTO.java │ │ │ │ ├── CourseProposalStatus.java │ │ │ │ ├── CourseProposalAlreadyDeclinedException.java │ │ │ │ ├── CourseProposalAlreadyApprovedException.java │ │ │ │ ├── CourseProposalRepository.java │ │ │ │ └── CourseProposal.java │ │ │ └── resources │ │ │ └── db │ │ │ └── administration.yml │ └── build.gradle.kts └── integration-events │ ├── build.gradle.kts │ └── src │ └── main │ └── java │ └── com │ └── educational │ └── platform │ └── administration │ └── integration │ └── event │ ├── CourseDeclinedByAdminIntegrationEvent.java │ └── CourseApprovedByAdminIntegrationEvent.java ├── courses ├── application │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── educational │ │ │ │ └── platform │ │ │ │ └── courses │ │ │ │ ├── course │ │ │ │ ├── LectureType.java │ │ │ │ ├── query │ │ │ │ │ ├── ListCourseQuery.java │ │ │ │ │ ├── CourseByUUIDQuery.java │ │ │ │ │ ├── ListCourseQueryHandler.java │ │ │ │ │ └── CourseByUUIDQueryHandler.java │ │ │ │ ├── QuestionDTO.java │ │ │ │ ├── PublishStatus.java │ │ │ │ ├── ApprovalStatus.java │ │ │ │ ├── approve │ │ │ │ │ ├── ApproveCourseCommand.java │ │ │ │ │ ├── SendCourseToApproveCommand.java │ │ │ │ │ ├── CourseApprovedByAdminIntegrationEventHandler.java │ │ │ │ │ └── ApproveCourseCommandHandler.java │ │ │ │ ├── publish │ │ │ │ │ ├── PublishCourseCommand.java │ │ │ │ │ └── CourseTeacherChecker.java │ │ │ │ ├── create │ │ │ │ │ ├── CreateCurriculumItemCommand.java │ │ │ │ │ ├── CreateQuestionCommand.java │ │ │ │ │ ├── CreateLectureCommand.java │ │ │ │ │ ├── CreateQuizCommand.java │ │ │ │ │ ├── CreateCourseCommand.java │ │ │ │ │ └── CreateCourseCommandHandler.java │ │ │ │ ├── numberofsudents │ │ │ │ │ └── update │ │ │ │ │ │ ├── IncreaseNumberOfStudentsCommand.java │ │ │ │ │ │ ├── StudentEnrolledToCourseIntegrationEventHandler.java │ │ │ │ │ │ └── IncreaseNumberOfStudentsCommandHandler.java │ │ │ │ ├── rating │ │ │ │ │ └── update │ │ │ │ │ │ ├── UpdateCourseRatingCommand.java │ │ │ │ │ │ ├── CourseRatingRecalculatedIntegrationEventHandler.java │ │ │ │ │ │ └── UpdateCourseRatingCommandHandler.java │ │ │ │ ├── CourseRating.java │ │ │ │ ├── NumberOfStudents.java │ │ │ │ ├── CourseLightDTO.java │ │ │ │ ├── CurriculumItemDTO.java │ │ │ │ ├── CourseRepositoryCustom.java │ │ │ │ ├── QuizDTO.java │ │ │ │ ├── LectureDTO.java │ │ │ │ ├── CourseAlreadyApprovedException.java │ │ │ │ ├── Lecture.java │ │ │ │ ├── CourseCannotBePublishedException.java │ │ │ │ ├── Question.java │ │ │ │ ├── Quiz.java │ │ │ │ ├── CurrentUserAsTeacher.java │ │ │ │ ├── CourseDTO.java │ │ │ │ ├── CurriculumItemFactory.java │ │ │ │ ├── CourseRepositoryCustomImpl.java │ │ │ │ ├── CourseFactory.java │ │ │ │ ├── CurriculumItem.java │ │ │ │ ├── CourseDTOResultTransformer.java │ │ │ │ └── CourseRepository.java │ │ │ │ └── teacher │ │ │ │ ├── create │ │ │ │ ├── CreateTeacherCommand.java │ │ │ │ ├── CreateTeacherCommandHandler.java │ │ │ │ └── UserCreatedIntegrationEventHandler.java │ │ │ │ ├── TeacherRepository.java │ │ │ │ └── Teacher.java │ │ └── test │ │ │ ├── java │ │ │ └── com │ │ │ │ └── educational │ │ │ │ └── platform │ │ │ │ ├── TestApplication.java │ │ │ │ └── courses │ │ │ │ ├── teacher │ │ │ │ └── create │ │ │ │ │ ├── CreateTeacherCommandHandlerTest.java │ │ │ │ │ └── integration │ │ │ │ │ └── CreateTeacherCommandHandlerIntegrationTest.java │ │ │ │ └── course │ │ │ │ ├── jpa │ │ │ │ └── CourseRepositoryTest.java │ │ │ │ ├── approve │ │ │ │ └── CourseApprovedByAdminIntegrationEventHandlerTest.java │ │ │ │ └── rating │ │ │ │ └── update │ │ │ │ └── CourseRatingRecalculatedIntegrationEventHandlerTest.java │ │ │ └── resources │ │ │ ├── approved_course.sql │ │ │ └── not_approved_course.sql │ └── build.gradle.kts ├── web │ ├── src │ │ ├── test │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── educational │ │ │ │ │ └── platform │ │ │ │ │ └── TestApplication.java │ │ │ └── resources │ │ │ │ └── insert_data.sql │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── educational │ │ │ └── platform │ │ │ └── courses │ │ │ ├── CreatedCourseResponse.java │ │ │ └── CreateCourseRequest.java │ └── build.gradle.kts └── integration-events │ ├── build.gradle.kts │ └── src │ └── main │ └── java │ └── com │ └── educational │ └── platform │ └── courses │ └── integration │ └── event │ └── SendCourseToApproveIntegrationEvent.java ├── .github └── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md ├── common ├── src │ └── main │ │ └── java │ │ └── com │ │ └── educational │ │ └── platform │ │ └── common │ │ ├── domain │ │ ├── ValueObject.java │ │ └── AggregateRoot.java │ │ └── exception │ │ ├── ResourceNotFoundException.java │ │ ├── UnprocessableEntityException.java │ │ └── RelatedResourceIsNotResolvedException.java └── build.gradle.kts ├── users ├── web │ ├── src │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── educational │ │ │ │ └── platform │ │ │ │ └── TestApplication.java │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── educational │ │ │ └── platform │ │ │ └── users │ │ │ └── security │ │ │ ├── SignInResponse.java │ │ │ ├── SignInRequest.java │ │ │ ├── SignUpRequest.java │ │ │ ├── JwtTokenFilterConfigurer.java │ │ │ ├── UserController.java │ │ │ └── JwtTokenFilter.java │ └── build.gradle.kts ├── application │ ├── src │ │ ├── test │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── educational │ │ │ │ └── platform │ │ │ │ └── TestApplication.java │ │ └── main │ │ │ ├── java │ │ │ └── com │ │ │ │ └── educational │ │ │ │ └── platform │ │ │ │ └── users │ │ │ │ ├── RoleDTO.java │ │ │ │ ├── UserDTO.java │ │ │ │ ├── login │ │ │ │ └── SignInCommand.java │ │ │ │ ├── ValidPassword.java │ │ │ │ ├── Role.java │ │ │ │ ├── registration │ │ │ │ └── UserRegistrationCommand.java │ │ │ │ ├── UserRepository.java │ │ │ │ ├── security │ │ │ │ └── MyUserDetails.java │ │ │ │ └── PasswordConstraintValidator.java │ │ │ └── resources │ │ │ └── db │ │ │ └── users.yml │ └── build.gradle.kts └── integration-events │ ├── build.gradle.kts │ └── src │ └── main │ └── java │ └── com │ └── educational │ └── platform │ └── users │ └── integration │ └── event │ └── UserCreatedIntegrationEvent.java ├── course-reviews ├── web │ ├── src │ │ ├── test │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── educational │ │ │ │ │ └── platform │ │ │ │ │ └── TestApplication.java │ │ │ └── resources │ │ │ │ └── insert_data.sql │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── educational │ │ │ └── platform │ │ │ └── course │ │ │ └── reviews │ │ │ ├── CourseReviewCreatedResponse.java │ │ │ ├── UpdateCourseReviewRequest.java │ │ │ └── ReviewCourseRequest.java │ └── build.gradle.kts ├── application │ ├── src │ │ ├── test │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── educational │ │ │ │ │ └── platform │ │ │ │ │ ├── TestApplication.java │ │ │ │ │ └── course │ │ │ │ │ └── reviews │ │ │ │ │ ├── jpa │ │ │ │ │ └── CourseReviewRepositoryTest.java │ │ │ │ │ ├── reviewer │ │ │ │ │ └── create │ │ │ │ │ │ └── integration │ │ │ │ │ │ └── CreateReviewerCommandHandlerIntegrationTest.java │ │ │ │ │ └── course │ │ │ │ │ └── create │ │ │ │ │ └── integration │ │ │ │ │ └── CreateReviewableCourseCommandHandlerIntegrationTest.java │ │ │ └── resources │ │ │ │ └── course_review.sql │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── educational │ │ │ └── platform │ │ │ └── course │ │ │ └── reviews │ │ │ ├── query │ │ │ ├── ListCourseReviewsByCourseUUIDQuery.java │ │ │ └── ListCourseReviewsByCourseUUIDQueryHandler.java │ │ │ ├── reviewer │ │ │ ├── create │ │ │ │ ├── CreateReviewerCommand.java │ │ │ │ └── CreateReviewerCommandHandler.java │ │ │ ├── ReviewerRepository.java │ │ │ └── Reviewer.java │ │ │ ├── CourseReviewDTO.java │ │ │ ├── course │ │ │ ├── create │ │ │ │ ├── CreateReviewableCourseCommand.java │ │ │ │ └── CreateReviewableCourseCommandHandler.java │ │ │ ├── ReviewableCourseRepository.java │ │ │ └── ReviewableCourse.java │ │ │ ├── Comment.java │ │ │ ├── CourseRating.java │ │ │ ├── CourseReviewChecker.java │ │ │ ├── create │ │ │ ├── ReviewCourseCommand.java │ │ │ └── ReviewCourseCommandHandler.java │ │ │ ├── edit │ │ │ └── UpdateCourseReviewCommand.java │ │ │ ├── CurrentUserAsReviewer.java │ │ │ └── CourseReview.java │ └── build.gradle.kts └── integration-events │ ├── build.gradle.kts │ └── src │ └── main │ └── java │ └── com │ └── educational │ └── platform │ └── course │ └── reviews │ └── integration │ └── event │ └── CourseRatingRecalculatedIntegrationEvent.java ├── course-enrollments ├── web │ ├── src │ │ ├── test │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── educational │ │ │ │ │ └── platform │ │ │ │ │ ├── TestApplication.java │ │ │ │ │ └── course │ │ │ │ │ └── enrollments │ │ │ │ │ └── api │ │ │ │ │ └── CourseEnrollmentApiTest.java │ │ │ └── resources │ │ │ │ └── insert_data.sql │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── educational │ │ │ └── platform │ │ │ └── course │ │ │ └── enrollments │ │ │ ├── CourseEnrollmentRequest.java │ │ │ └── CourseEnrollmentController.java │ └── build.gradle.kts ├── application │ ├── src │ │ ├── test │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── educational │ │ │ │ │ └── platform │ │ │ │ │ ├── TestApplication.java │ │ │ │ │ └── course │ │ │ │ │ └── enrollments │ │ │ │ │ ├── CourseEnrollmentTest.java │ │ │ │ │ ├── query │ │ │ │ │ └── security │ │ │ │ │ │ ├── CourseEnrollmentByUUIDQueryHandlerSecurityTest.java │ │ │ │ │ │ └── ListCourseEnrollmentsQueryHandlerSecurityTest.java │ │ │ │ │ └── student │ │ │ │ │ └── create │ │ │ │ │ └── CreateStudentCommandHandlerIntegrationTest.java │ │ │ └── resources │ │ │ │ └── course.sql │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── educational │ │ │ └── platform │ │ │ └── course │ │ │ └── enrollments │ │ │ ├── CompletionStatusDTO.java │ │ │ ├── query │ │ │ ├── ListCourseEnrollmentsQuery.java │ │ │ ├── CourseEnrollmentByUUIDQuery.java │ │ │ ├── ListCourseEnrollmentsQueryHandler.java │ │ │ └── CourseEnrollmentByUUIDQueryHandler.java │ │ │ ├── student │ │ │ ├── create │ │ │ │ ├── CreateStudentCommand.java │ │ │ │ └── CreateStudentCommandHandler.java │ │ │ ├── StudentRepository.java │ │ │ └── Student.java │ │ │ ├── course │ │ │ ├── create │ │ │ │ ├── CreateCourseCommand.java │ │ │ │ └── CreateEnrollmentCourseCommandHandler.java │ │ │ ├── EnrollCourseRepository.java │ │ │ └── EnrollCourse.java │ │ │ ├── register │ │ │ └── RegisterStudentToCourseCommand.java │ │ │ ├── CompletionStatus.java │ │ │ ├── CourseEnrollmentDTO.java │ │ │ ├── CurrentUserAsStudent.java │ │ │ └── CourseEnrollment.java │ └── build.gradle.kts └── integration-events │ ├── build.gradle.kts │ └── src │ └── main │ └── java │ └── com │ └── educational │ └── platform │ └── course │ └── enrollments │ └── integration │ └── event │ └── StudentEnrolledToCourseIntegrationEvent.java ├── configuration ├── src │ ├── main │ │ ├── resources │ │ │ ├── application.properties │ │ │ └── db │ │ │ │ └── changelog │ │ │ │ └── db.changelog-master.yml │ │ └── java │ │ │ └── com │ │ │ └── educational │ │ │ └── platform │ │ │ ├── EducationalPlatformApplication.java │ │ │ └── AxonConfiguration.java │ └── test │ │ └── java │ │ └── com │ │ └── educational │ │ └── platform │ │ ├── event │ │ └── IntegrationEventTest.java │ │ ├── layer │ │ └── LayerTest.java │ │ └── handler │ │ └── CommandHandlerTest.java └── build.gradle.kts ├── .gitignore ├── web ├── build.gradle.kts └── src │ └── main │ └── java │ └── com │ └── educational │ └── platform │ └── web │ └── handler │ └── ErrorResponse.java └── LICENSE /security/config/src/main/resources/application-security.properties: -------------------------------------------------------------------------------- 1 | com.educational.platform.security.enabled=true 2 | -------------------------------------------------------------------------------- /docs/architecture_tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anton-liauchuk/educational-platform/HEAD/docs/architecture_tests.png -------------------------------------------------------------------------------- /docs/bounded_context_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anton-liauchuk/educational-platform/HEAD/docs/bounded_context_map.png -------------------------------------------------------------------------------- /docs/integration_events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anton-liauchuk/educational-platform/HEAD/docs/integration_events.png -------------------------------------------------------------------------------- /docs/global_module_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anton-liauchuk/educational-platform/HEAD/docs/global_module_structure.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anton-liauchuk/educational-platform/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /docs/business_submodules_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anton-liauchuk/educational-platform/HEAD/docs/business_submodules_structure.png -------------------------------------------------------------------------------- /security/config/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation("org.springframework.boot", "spring-boot-starter-security") 3 | } 4 | 5 | -------------------------------------------------------------------------------- /administration/web/src/test/resources/insert_data.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM course_proposal; 2 | 3 | INSERT INTO course_proposal (uuid, status) VALUES ('123E4567E89B12D3A456426655440001', 'WAITING_FOR_APPROVAL'); 4 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/LectureType.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | public enum LectureType { 4 | 5 | TEXT 6 | 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /common/src/main/java/com/educational/platform/common/domain/ValueObject.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.common.domain; 2 | 3 | /** 4 | * Represents Value Object marker. 5 | */ 6 | public interface ValueObject { 7 | } 8 | -------------------------------------------------------------------------------- /common/src/main/java/com/educational/platform/common/domain/AggregateRoot.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.common.domain; 2 | 3 | /** 4 | * Represents Aggregate Root marker. 5 | */ 6 | public interface AggregateRoot { 7 | } 8 | -------------------------------------------------------------------------------- /users/web/src/test/java/com/educational/platform/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /courses/web/src/test/java/com/educational/platform/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /course-reviews/web/src/test/java/com/educational/platform/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /courses/application/src/test/java/com/educational/platform/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /users/application/src/test/java/com/educational/platform/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /course-enrollments/web/src/test/java/com/educational/platform/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /administration/application/src/test/java/com/educational/platform/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api("org.axonframework", "axon-spring-boot-starter", libs.versions.axon.get()) { 3 | exclude(group = "org.axonframework", module = "axon-server-connector") 4 | } 5 | 6 | runtimeOnly("com.h2database", "h2") 7 | } -------------------------------------------------------------------------------- /course-reviews/application/src/test/java/com/educational/platform/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /course-enrollments/application/src/test/java/com/educational/platform/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/query/ListCourseQuery.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.query; 2 | 3 | /** 4 | * Represents course query for retrieving list of courses. 5 | */ 6 | public class ListCourseQuery { 7 | } 8 | -------------------------------------------------------------------------------- /course-enrollments/web/src/test/resources/insert_data.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM course_enrollment; 2 | DELETE FROM enroll_course; 3 | DELETE FROM student; 4 | 5 | INSERT INTO enroll_course (uuid) VALUES ('123E4567E89B12D3A456426655440001'); 6 | INSERT INTO student (username) VALUES ('username'); 7 | -------------------------------------------------------------------------------- /users/application/src/main/java/com/educational/platform/users/RoleDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users; 2 | 3 | /** 4 | * Possible values of roles during registration. 5 | */ 6 | public enum RoleDTO { 7 | 8 | ROLE_STUDENT, 9 | ROLE_TEACHER; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /course-reviews/web/src/test/resources/insert_data.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM course_review; 2 | DELETE FROM reviewable_course; 3 | DELETE FROM reviewer; 4 | 5 | INSERT INTO reviewable_course (original_course_id) VALUES ('123E4567E89B12D3A456426655440001'); 6 | INSERT INTO reviewer (username) VALUES ('username'); 7 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/QuestionDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | public class QuestionDTO { 4 | 5 | String content; 6 | 7 | public QuestionDTO(String content) { 8 | this.content = content; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/PublishStatus.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | /** 4 | * Represents Course Publish Status. 5 | */ 6 | public enum PublishStatus { 7 | 8 | DRAFT, 9 | PUBLISHED, 10 | ARCHIVED 11 | 12 | } 13 | -------------------------------------------------------------------------------- /administration/integration-events/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation("org.springframework", "spring-context") 4 | 5 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 6 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 7 | } -------------------------------------------------------------------------------- /courses/integration-events/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation("org.springframework", "spring-context") 4 | 5 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 6 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 7 | } 8 | -------------------------------------------------------------------------------- /course-enrollments/integration-events/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation("org.springframework", "spring-context") 4 | 5 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 6 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 7 | } -------------------------------------------------------------------------------- /users/integration-events/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation("org.springframework", "spring-context") 4 | 5 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 6 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 7 | } 8 | 9 | -------------------------------------------------------------------------------- /course-reviews/integration-events/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation("org.springframework", "spring-context") 4 | 5 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 6 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 7 | } 8 | 9 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0010-use-axon-framework.md: -------------------------------------------------------------------------------- 1 | # 10. Axon Framework. 2 | Date: 2020-07-06 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | Currently, a lot of custom classes are defined for DDD building blocks. It will be better to use DDD library for these goals. 9 | 10 | ## Decision 11 | Axon Framework will be used as DDD library. 12 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/query/ListCourseProposalsQuery.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.query; 2 | 3 | import lombok.Value; 4 | 5 | /** 6 | * Represents list course proposals query. 7 | */ 8 | @Value 9 | public class ListCourseProposalsQuery { 10 | } 11 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0009-architecture-tests.md: -------------------------------------------------------------------------------- 1 | # 9. Architecture tests. 2 | Date: 2020-06-27 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | We need to have the mechanism for supporting and validating common architecture principles in all application. 9 | 10 | ## Decision 11 | Architecture tests with using Archunit should be implemented. 12 | -------------------------------------------------------------------------------- /course-enrollments/application/src/test/resources/course.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM course_enrollment; 2 | DELETE FROM enroll_course; 3 | DELETE FROM student; 4 | 5 | INSERT INTO student (username) VALUES ('student'); 6 | INSERT INTO enroll_course (uuid) VALUES ('123E4567E89B12D3A456426655440001'); 7 | INSERT INTO enroll_course (uuid) VALUES ('123E4567E89B12D3A456426655440002'); 8 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/approve/ApproveCourseProposalCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.approve; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * Approve course proposal command. 7 | */ 8 | public record ApproveCourseProposalCommand(UUID uuid) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/create/CreateCourseProposalCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.create; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * Represents Create Course Proposal Command. 7 | */ 8 | public record CreateCourseProposalCommand(UUID uuid) { 9 | } 10 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/decline/DeclineCourseProposalCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.decline; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * Decline course proposal command. 7 | */ 8 | public record DeclineCourseProposalCommand(UUID uuid) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/ApprovalStatus.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | /** 4 | * Represents Course Approval Status. 5 | */ 6 | public enum ApprovalStatus { 7 | 8 | NOT_SENT_FOR_APPROVAL, 9 | WAITING_FOR_APPROVAL, 10 | DECLINED, 11 | APPROVED 12 | 13 | } 14 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/CompletionStatusDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments; 2 | 3 | /** 4 | * Represents possible values for completion status of enrollment in dto 5 | */ 6 | public enum CompletionStatusDTO { 7 | 8 | IN_PROGRESS, 9 | COMPLETED 10 | 11 | } 12 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/query/ListCourseEnrollmentsQuery.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.query; 2 | 3 | import lombok.Value; 4 | 5 | /** 6 | * Represents course enrollments query for retrieving course enrollments. 7 | */ 8 | @Value 9 | public class ListCourseEnrollmentsQuery { 10 | } 11 | -------------------------------------------------------------------------------- /administration/web/src/test/java/com/educational/platform/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; 5 | 6 | @SpringBootApplication(exclude = SecurityAutoConfiguration.class) 7 | public class TestApplication { 8 | } 9 | -------------------------------------------------------------------------------- /common/src/main/java/com/educational/platform/common/exception/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.common.exception; 2 | 3 | /** 4 | * Represents Resource Not Found Exception. 5 | */ 6 | public class ResourceNotFoundException extends RuntimeException { 7 | 8 | public ResourceNotFoundException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /configuration/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yml 2 | spring.datasource.url=jdbc:h2:mem:testdb 3 | spring.datasource.driverClassName=org.h2.Driver 4 | spring.datasource.username=sa 5 | spring.datasource.password=password 6 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 7 | spring.h2.console.enabled=true 8 | -------------------------------------------------------------------------------- /security/config/src/main/java/com/educational/platform/security/JwtTokenValidationException.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.security; 2 | 3 | /** 4 | * Represents jwt token validation exception 5 | */ 6 | public class JwtTokenValidationException extends RuntimeException { 7 | 8 | public JwtTokenValidationException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/CourseProposalStatusDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course; 2 | 3 | /** 4 | * Represents possible values for course proposal status in dto 5 | */ 6 | public enum CourseProposalStatusDTO { 7 | 8 | WAITING_FOR_APPROVAL, 9 | DECLINED, 10 | APPROVED 11 | 12 | } 13 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/query/CourseByUUIDQuery.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.query; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.Value; 6 | 7 | /** 8 | * Represents course query for retrieving course by uuid. 9 | */ 10 | @Value 11 | public class CourseByUUIDQuery { 12 | 13 | UUID uuid; 14 | } 15 | -------------------------------------------------------------------------------- /courses/application/src/test/resources/approved_course.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM course; 2 | DELETE FROM teacher; 3 | 4 | INSERT INTO teacher (username) VALUES ('teacher'); 5 | INSERT INTO course (uuid, approval_status, number, rating) VALUES ('123E4567E89B12D3A456426655440001', 'APPROVED', 0, 0); 6 | UPDATE course SET teacher = (SELECT teacher.id FROM teacher WHERE teacher.username = 'teacher' GROUP BY teacher.id); 7 | -------------------------------------------------------------------------------- /common/src/main/java/com/educational/platform/common/exception/UnprocessableEntityException.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.common.exception; 2 | 3 | /** 4 | * Represents Unprocessable Entity Exception. 5 | */ 6 | public class UnprocessableEntityException extends RuntimeException { 7 | 8 | public UnprocessableEntityException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /courses/application/src/test/resources/not_approved_course.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM course; 2 | DELETE FROM teacher; 3 | 4 | INSERT INTO teacher (username) VALUES ('teacher'); 5 | INSERT INTO course (uuid, approval_status, number, rating) VALUES ('123E4567E89B12D3A456426655440001', 'NOT_SENT_FOR_APPROVAL', 0, 0); 6 | UPDATE course SET teacher = (SELECT teacher.id FROM teacher WHERE teacher.username = 'teacher' GROUP BY teacher.id); 7 | -------------------------------------------------------------------------------- /configuration/src/main/resources/db/changelog/db.changelog-master.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - include: 3 | file: "classpath*:/db/administration.yml" 4 | - include: 5 | file: "classpath*:/db/course-enrollments.yml" 6 | - include: 7 | file: "classpath*:/db/course-reviews.yml" 8 | - include: 9 | file: "classpath*:/db/courses.yml" 10 | - include: 11 | file: "classpath*:/db/users.yml" 12 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/teacher/create/CreateTeacherCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.teacher.create; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | /** 7 | * Create teacher command. 8 | */ 9 | @Data 10 | @AllArgsConstructor 11 | public class CreateTeacherCommand { 12 | 13 | private final String username; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /users/integration-events/src/main/java/com/educational/platform/users/integration/event/UserCreatedIntegrationEvent.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users.integration.event; 2 | 3 | import lombok.Value; 4 | 5 | /** 6 | * Represents user created integration event, should be published after user creation. 7 | */ 8 | @Value 9 | public class UserCreatedIntegrationEvent { 10 | 11 | String username, email; 12 | } 13 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/query/ListCourseReviewsByCourseUUIDQuery.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.query; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.Value; 6 | 7 | /** 8 | * Represents list course reviews by course uuid query. 9 | */ 10 | @Value 11 | public class ListCourseReviewsByCourseUUIDQuery { 12 | 13 | UUID uuid; 14 | } 15 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/teacher/TeacherRepository.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.teacher; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | /** 6 | * Represents teacher repository. 7 | */ 8 | public interface TeacherRepository extends JpaRepository { 9 | 10 | Teacher findByUsername(String username); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /courses/web/src/test/resources/insert_data.sql: -------------------------------------------------------------------------------- 1 | delete from course; 2 | delete from teacher; 3 | 4 | INSERT INTO teacher (username) VALUES ('username'); 5 | INSERT INTO course (uuid, name, description, approval_status, number, rating) VALUES ('123E4567E89B12D3A456426655440001', 'course name', 'description', 'APPROVED', 0, 0); 6 | UPDATE course SET teacher = (SELECT teacher.id FROM teacher WHERE teacher.username = 'username' GROUP BY teacher.id); 7 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0008-result-from-comand-handlers.md: -------------------------------------------------------------------------------- 1 | # 8. Results from command handlers. 2 | Date: 2020-05-14 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | The idea from CQRS - do not return anything from command processing. But in some cases, we need to get generated identifiers of new created resources. 9 | 10 | ## Decision 11 | Command handlers can return generated identifiers after processing if it's needed. 12 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0006-rich-domain-model.md: -------------------------------------------------------------------------------- 1 | # 6. Rich domain model. 2 | Date: 2020-05-05 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | Possible solutions related to domain model implementation: 9 | - anemic domain model without any logic inside; 10 | - rich domain model with encapsulated logic; 11 | 12 | ## Decision 13 | Rich domain model solution will be used. Domain model will encapsulate internal structure and logic. 14 | -------------------------------------------------------------------------------- /users/web/src/main/java/com/educational/platform/users/security/SignInResponse.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users.security; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | /** 8 | * Represents the response after sign in operation. 9 | */ 10 | @Builder 11 | @Data 12 | @AllArgsConstructor 13 | public class SignInResponse { 14 | 15 | private final String token; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /security/test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":users:users-web")) 3 | implementation("org.springframework.boot", "spring-boot-starter-security") 4 | implementation("io.rest-assured", "rest-assured", libs.versions.restAssured.get()) 5 | implementation("io.rest-assured", "json-path", libs.versions.restAssured.get()) 6 | implementation("io.rest-assured", "xml-path", libs.versions.restAssured.get()) 7 | } 8 | 9 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/query/CourseEnrollmentByUUIDQuery.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.query; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.Value; 6 | 7 | /** 8 | * Represents course enrollment query for retrieving course enrollment by uuid. 9 | */ 10 | @Value 11 | public class CourseEnrollmentByUUIDQuery { 12 | 13 | UUID uuid; 14 | } 15 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/approve/ApproveCourseCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.approve; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | import java.util.UUID; 7 | 8 | /** 9 | * Approve course command. 10 | */ 11 | @Data 12 | @AllArgsConstructor 13 | public class ApproveCourseCommand { 14 | 15 | private final UUID uuid; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/publish/PublishCourseCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.publish; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | import java.util.UUID; 7 | 8 | /** 9 | * Publish course command. 10 | */ 11 | @Data 12 | @AllArgsConstructor 13 | public class PublishCourseCommand { 14 | 15 | private final UUID uuid; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/create/CreateCurriculumItemCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.create; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public abstract class CreateCurriculumItemCommand { 9 | 10 | private final String title; 11 | private final String description; 12 | private final Integer serialNumber; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /common/src/main/java/com/educational/platform/common/exception/RelatedResourceIsNotResolvedException.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.common.exception; 2 | 3 | /** 4 | * Represents exception for the cases when related resource is not resolved 5 | */ 6 | public class RelatedResourceIsNotResolvedException extends RuntimeException { 7 | 8 | public RelatedResourceIsNotResolvedException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/reviewer/create/CreateReviewerCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.reviewer.create; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | /** 7 | * Represents Create Reviewer Command. 8 | */ 9 | @Data 10 | @AllArgsConstructor 11 | public class CreateReviewerCommand { 12 | 13 | private final String username; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/student/create/CreateStudentCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.student.create; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | /** 7 | * Represents Create student command. 8 | */ 9 | @Data 10 | @AllArgsConstructor 11 | public class CreateStudentCommand { 12 | 13 | private final String username; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/approve/SendCourseToApproveCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.approve; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | import java.util.UUID; 7 | 8 | /** 9 | * Send course to approve command. 10 | */ 11 | @Data 12 | @AllArgsConstructor 13 | public class SendCourseToApproveCommand { 14 | 15 | private final UUID uuid; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | spring = "3.5.4" 3 | restAssured = "5.5.5" 4 | mockito = "5.19.0" 5 | assertj = "3.27.3" 6 | archunit = "1.4.1" 7 | axon = "4.12.1" 8 | lombok = "1.18.32" 9 | jsonwebtoken = "0.9.1" 10 | jaxbApi = "2.3.1" 11 | passay = "1.6.4" 12 | springDependencyManagementPlugin = "1.1.7" 13 | springDoc = "2.8.13" 14 | 15 | [plugins] 16 | springdependencies = { id = "io.spring.dependency-management", version.ref = "springDependencyManagementPlugin" } 17 | -------------------------------------------------------------------------------- /users/application/src/main/java/com/educational/platform/users/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | /** 8 | * Represents User DTO. 9 | */ 10 | @Builder 11 | @Data 12 | @AllArgsConstructor 13 | public class UserDTO { 14 | 15 | private final String username; 16 | private final String email; 17 | private final RoleDTO role; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/course/create/CreateCourseCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.course.create; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | import java.util.UUID; 7 | 8 | /** 9 | * Represents Create Course Command. 10 | */ 11 | @Data 12 | @AllArgsConstructor 13 | public class CreateCourseCommand { 14 | 15 | private final UUID uuid; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /courses/web/src/main/java/com/educational/platform/courses/CreatedCourseResponse.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import java.util.UUID; 8 | 9 | /** 10 | * Represents Course API DTO which used as response for create operation. 11 | */ 12 | @Builder 13 | @Data 14 | @AllArgsConstructor 15 | public class CreatedCourseResponse { 16 | 17 | private final UUID uuid; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/CourseReviewDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Value; 8 | 9 | /** 10 | * Represents course review dto. 11 | */ 12 | @Builder 13 | @AllArgsConstructor 14 | @Value 15 | public class CourseReviewDTO { 16 | 17 | UUID uuid, course; 18 | String username, comment; 19 | double rating; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/course/create/CreateReviewableCourseCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.course.create; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | import java.util.UUID; 7 | 8 | /** 9 | * Represents Create Reviewable Course Command. 10 | */ 11 | @Data 12 | @AllArgsConstructor 13 | public class CreateReviewableCourseCommand { 14 | 15 | private final UUID uuid; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation("org.springframework.boot", "spring-boot-starter-web") 4 | implementation("org.springframework.boot", "spring-boot-starter-validation") 5 | implementation("org.springdoc", "springdoc-openapi-starter-webmvc-ui", libs.versions.springDoc.get()) 6 | 7 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 8 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 9 | } 10 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/create/CreateQuestionCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.create; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | 9 | /** 10 | * Create question command. 11 | */ 12 | @Builder 13 | @Data 14 | @AllArgsConstructor 15 | public class CreateQuestionCommand { 16 | 17 | @NotBlank 18 | private final String content; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /course-reviews/web/src/main/java/com/educational/platform/course/reviews/CourseReviewCreatedResponse.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import java.util.UUID; 8 | 9 | /** 10 | * Represents response model for new created course review. 11 | */ 12 | @Builder 13 | @Data 14 | @AllArgsConstructor 15 | public class CourseReviewCreatedResponse { 16 | 17 | private final UUID uuid; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/numberofsudents/update/IncreaseNumberOfStudentsCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.numberofsudents.update; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import java.util.UUID; 7 | 8 | /** 9 | * Increase number of students command. 10 | */ 11 | @Data 12 | @RequiredArgsConstructor 13 | public class IncreaseNumberOfStudentsCommand { 14 | 15 | private final UUID uuid; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/rating/update/UpdateCourseRatingCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.rating.update; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import java.util.UUID; 7 | 8 | /** 9 | * Update course rating command. 10 | */ 11 | // todo immutable 12 | @Data 13 | @RequiredArgsConstructor 14 | public class UpdateCourseRatingCommand { 15 | 16 | private final UUID uuid; 17 | private final double rating; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /users/application/src/main/java/com/educational/platform/users/login/SignInCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users.login; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import jakarta.validation.constraints.NotBlank; 8 | 9 | /** 10 | * Represents Sign In Command. 11 | */ 12 | @Builder 13 | @Data 14 | @AllArgsConstructor 15 | public class SignInCommand { 16 | 17 | @NotBlank 18 | private final String username; 19 | 20 | @NotBlank 21 | private final String password; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /course-reviews/integration-events/src/main/java/com/educational/platform/course/reviews/integration/event/CourseRatingRecalculatedIntegrationEvent.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.integration.event; 2 | 3 | import lombok.Value; 4 | 5 | import java.util.UUID; 6 | 7 | /** 8 | * Represents course rating recalculated integration event, should be published after course rating recalculation. 9 | */ 10 | @Value 11 | public class CourseRatingRecalculatedIntegrationEvent { 12 | 13 | UUID courseId; 14 | double rating; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/Comment.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews; 2 | 3 | import com.educational.platform.common.domain.ValueObject; 4 | import lombok.*; 5 | 6 | import jakarta.persistence.Embeddable; 7 | 8 | /** 9 | * Represents Comment model. 10 | */ 11 | @Embeddable 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | @Getter 15 | @EqualsAndHashCode 16 | @ToString 17 | public class Comment implements ValueObject { 18 | 19 | private String comment; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CourseRating.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import com.educational.platform.common.domain.ValueObject; 4 | import lombok.*; 5 | 6 | import jakarta.persistence.Embeddable; 7 | 8 | /** 9 | * Represents Course Rating model. 10 | */ 11 | @Embeddable 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | @Getter 15 | @EqualsAndHashCode 16 | @ToString 17 | public class CourseRating implements ValueObject { 18 | 19 | private double rating; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/CourseRating.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews; 2 | 3 | import com.educational.platform.common.domain.ValueObject; 4 | import lombok.*; 5 | 6 | import jakarta.persistence.Embeddable; 7 | 8 | /** 9 | * Represents Course Rating model. 10 | */ 11 | @Embeddable 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | @Getter 15 | @EqualsAndHashCode 16 | @ToString 17 | public class CourseRating implements ValueObject { 18 | 19 | private double rating; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/NumberOfStudents.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import com.educational.platform.common.domain.ValueObject; 4 | import lombok.*; 5 | 6 | import jakarta.persistence.Embeddable; 7 | 8 | /** 9 | * Represents number of students model. 10 | */ 11 | @Embeddable 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | @Getter 15 | @EqualsAndHashCode 16 | @ToString 17 | public class NumberOfStudents implements ValueObject { 18 | 19 | private int number; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/create/CreateLectureCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.create; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class CreateLectureCommand extends CreateCurriculumItemCommand { 10 | 11 | private final String text; 12 | 13 | @Builder 14 | public CreateLectureCommand(String title, String description, Integer serialNumber, String text) { 15 | super(title, description, serialNumber); 16 | this.text = text; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /web/src/main/java/com/educational/platform/web/handler/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.web.handler; 2 | 3 | 4 | import lombok.Data; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | /** 11 | * Represents error response. 12 | */ 13 | @RequiredArgsConstructor 14 | @Data 15 | public class ErrorResponse { 16 | 17 | private final List errors; 18 | 19 | public ErrorResponse(String error) { 20 | this(error != null ? Collections.singletonList(error) : Collections.emptyList()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/register/RegisterStudentToCourseCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.register; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import jakarta.validation.constraints.NotNull; 8 | import java.util.UUID; 9 | 10 | /** 11 | * Represents register student to course command. 12 | */ 13 | @Builder 14 | @Data 15 | @AllArgsConstructor 16 | public class RegisterStudentToCourseCommand { 17 | 18 | @NotNull 19 | private final UUID courseId; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /courses/web/src/main/java/com/educational/platform/courses/CreateCourseRequest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import jakarta.validation.constraints.NotBlank; 9 | 10 | /** 11 | * Represents Course Create Request. 12 | */ 13 | @Builder 14 | @Data 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class CreateCourseRequest { 18 | 19 | @NotBlank 20 | private String name; 21 | 22 | @NotBlank 23 | private String description; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /users/web/src/main/java/com/educational/platform/users/security/SignInRequest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users.security; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | /** 11 | * Represents Course Create Request. 12 | */ 13 | @Builder 14 | @Data 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class SignInRequest { 18 | 19 | @NotBlank 20 | private String username; 21 | 22 | @NotBlank 23 | private String password; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /course-enrollments/web/src/main/java/com/educational/platform/course/enrollments/CourseEnrollmentRequest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import jakarta.validation.constraints.NotNull; 9 | 10 | /** 11 | * Represents course enrollment request. 12 | */ 13 | @Builder 14 | @Data 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class CourseEnrollmentRequest { 18 | 19 | // todo get student from current user 20 | @NotNull 21 | private String student; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CourseLightDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.Value; 6 | 7 | /** 8 | * Represents course light dto. 9 | */ 10 | @Value 11 | public class CourseLightDTO { 12 | 13 | UUID uuid; 14 | String name, description; 15 | int numberOfStudents; 16 | 17 | public CourseLightDTO(UUID uuid, String name, String description, NumberOfStudents numberOfStudents) { 18 | this.uuid = uuid; 19 | this.name = name; 20 | this.description = description; 21 | this.numberOfStudents = numberOfStudents.getNumber(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/CompletionStatus.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments; 2 | 3 | /** 4 | * Represents possible values for completion status of enrollment 5 | */ 6 | public enum CompletionStatus { 7 | 8 | IN_PROGRESS, 9 | COMPLETED; 10 | 11 | public CompletionStatusDTO toDTO() { 12 | switch (this) { 13 | case IN_PROGRESS: 14 | return CompletionStatusDTO.IN_PROGRESS; 15 | case COMPLETED: 16 | return CompletionStatusDTO.COMPLETED; 17 | } 18 | 19 | return null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CurriculumItemDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.AllArgsConstructor; 6 | 7 | @AllArgsConstructor 8 | public abstract class CurriculumItemDTO { 9 | 10 | public static final String TYPE = "curriculumItems_type"; 11 | public static final String TITLE = "curriculumItems_title"; 12 | public static final String DESCRIPTION = "curriculumItems_description"; 13 | public static final String SERIAL_NUMBER = "curriculumItems_serialNumber"; 14 | 15 | UUID uuid; 16 | String title, description; 17 | Integer serialNumber; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/CourseProposalDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Value; 8 | 9 | /** 10 | * Represents DTO for {@link CourseProposal} 11 | */ 12 | @Builder 13 | @AllArgsConstructor 14 | @Value 15 | public class CourseProposalDTO { 16 | 17 | UUID uuid; 18 | CourseProposalStatusDTO status; 19 | 20 | public CourseProposalDTO(UUID uuid, CourseProposalStatus status) { 21 | this.uuid = uuid; 22 | this.status = status.toDTO(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/CourseProposalStatus.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course; 2 | 3 | /** 4 | * Represents possible values for course proposal status 5 | */ 6 | public enum CourseProposalStatus { 7 | 8 | WAITING_FOR_APPROVAL, 9 | DECLINED, 10 | APPROVED; 11 | 12 | public CourseProposalStatusDTO toDTO() { 13 | return switch (this) { 14 | case WAITING_FOR_APPROVAL -> CourseProposalStatusDTO.WAITING_FOR_APPROVAL; 15 | case DECLINED -> CourseProposalStatusDTO.DECLINED; 16 | case APPROVED -> CourseProposalStatusDTO.APPROVED; 17 | }; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CourseRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | 6 | import org.springframework.data.repository.query.Param; 7 | 8 | public interface CourseRepositoryCustom { 9 | 10 | /** 11 | * Retrieves a course dto by its uuid. 12 | * 13 | * @param uuid must not be {@literal null}. 14 | * @return the course dto with the given uuid or {@literal Optional#empty()} if none found. 15 | * @throws IllegalArgumentException if {@literal uuid} is {@literal null}. 16 | */ 17 | Optional findDTOByUuid(@Param("uuid") UUID uuid); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0005-identifier-between-modules.md: -------------------------------------------------------------------------------- 1 | # 5. Identifier between modules. 2 | Date: 2020-04-28 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | During communications between bounded contexts, global identifiers for entities are needed. Also, these identifiers are needed for possible future integrations with external systems. 9 | 10 | ## Decision 11 | Natural keys or uuids should be used. Primary keys are forbidden for communications between modules or with external systems. If entity has good natural key - it's the most preferable choice for identifier between modules. 12 | 13 | Useful links: 14 | - https://tomharrisonjr.com/uuid-or-guid-as-primary-keys-be-careful-7b2aa3dcb439 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/QuizDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.UUID; 7 | 8 | public class QuizDTO extends CurriculumItemDTO { 9 | 10 | List questions; 11 | 12 | public QuizDTO(UUID uuid, Object[] tuple, Map aliasToIndexMap) { 13 | super(uuid, (String) tuple[aliasToIndexMap.get(CurriculumItemDTO.TITLE)], (String) tuple[aliasToIndexMap.get(CurriculumItemDTO.DESCRIPTION)], (Integer) tuple[aliasToIndexMap.get(CurriculumItemDTO.SERIAL_NUMBER)]); 14 | this.questions = new ArrayList<>(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /configuration/src/main/java/com/educational/platform/EducationalPlatformApplication.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.PropertySource; 6 | import org.springframework.scheduling.annotation.EnableAsync; 7 | 8 | @EnableAsync 9 | @SpringBootApplication 10 | @PropertySource("application-security.properties") 11 | public class EducationalPlatformApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(EducationalPlatformApplication.class, args); 15 | } 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/create/CreateQuizCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.create; 2 | 3 | import java.util.List; 4 | 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | 8 | @Getter 9 | public class CreateQuizCommand extends CreateCurriculumItemCommand { 10 | 11 | private final String text; 12 | private final List questions; 13 | 14 | @Builder 15 | public CreateQuizCommand(List questions, String title, String description, Integer serialNumber, String text) { 16 | super(title, description, serialNumber); 17 | this.text = text; 18 | this.questions = questions; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/create/CreateCourseCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.create; 2 | 3 | import java.util.List; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | 9 | import jakarta.validation.constraints.NotBlank; 10 | 11 | /** 12 | * Create course command. 13 | */ 14 | @Builder 15 | @Data 16 | @AllArgsConstructor 17 | public class CreateCourseCommand { 18 | 19 | @NotBlank 20 | private final String name; 21 | 22 | @NotBlank 23 | private final String description; 24 | 25 | private final List curriculumItems; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0011-use-axon-event-publishing-mechanism.md: -------------------------------------------------------------------------------- 1 | # 11. Integration events implementation by using Axon Framework. 2 | Date: 2021-01-10 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | In [0002-integration-events-implementation.md](0002-integration-events-implementation.md) was defined the solution for using Spring-event related classes for publishing and listening integration events. In current implementation of application we have Axon Framework which have rich tools for implementing such functionality. After migrating to Axon implementation of integration events, in future, we can enable event sourcing. 9 | 10 | ## Decision 11 | Axon Framework will be used for integration events implementation. 12 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0012-make-application-framework-independent.md: -------------------------------------------------------------------------------- 1 | # 12. Make Application Framework Independent 2 | Date: 2025-09-24 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | The current application is tightly coupled to Spring. This creates difficulties if we want to switch to another framework (e.g., Quarkus) or run parts of the system without Spring. To align with hexagonal architecture principles, the core domain should be independent from any framework. 9 | 10 | ## Decision 11 | We will refactor the application to remove framework-specific details from the core modules. As a next step, we will attempt to integrate Quarkus as an alternative framework, verifying the framework independence of the core modules. -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/LectureDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.Map; 4 | import java.util.UUID; 5 | 6 | public class LectureDTO extends CurriculumItemDTO { 7 | 8 | public static final String TEXT = "text"; 9 | 10 | String text; 11 | 12 | public LectureDTO(UUID uuid, Object[] tuples, Map aliasToIndexMap) { 13 | super(uuid, (String) tuples[aliasToIndexMap.get(CurriculumItemDTO.TITLE)], (String) tuples[aliasToIndexMap.get(CurriculumItemDTO.DESCRIPTION)], (Integer) tuples[aliasToIndexMap.get(CurriculumItemDTO.SERIAL_NUMBER)]); 14 | this.text = (String) tuples[aliasToIndexMap.get(LectureDTO.TEXT)]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /configuration/src/test/java/com/educational/platform/event/IntegrationEventTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.event; 2 | 3 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields; 4 | 5 | import com.tngtech.archunit.junit.AnalyzeClasses; 6 | import com.tngtech.archunit.junit.ArchTest; 7 | import com.tngtech.archunit.lang.ArchRule; 8 | 9 | @AnalyzeClasses(packages = "com.educational.platform") 10 | public class IntegrationEventTest { 11 | 12 | @ArchTest 13 | public static final ArchRule events_shouldBe_immutable = fields() 14 | .that() 15 | .areDeclaredInClassesThat() 16 | .haveSimpleNameEndingWith("Event") 17 | .should() 18 | .beFinal() 19 | .because("Events should be immutable."); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CourseAlreadyApprovedException.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * Course cannot be sent for approve exception because course is already approved 7 | */ 8 | public class CourseAlreadyApprovedException extends RuntimeException { 9 | 10 | private final UUID uuid; 11 | 12 | public CourseAlreadyApprovedException(UUID uuid) { 13 | super(); 14 | this.uuid = uuid; 15 | } 16 | 17 | @Override 18 | public String getMessage() { 19 | return "Course with uuid = " + uuid + " cannot be sent for approval, course was already approved"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /courses/integration-events/src/main/java/com/educational/platform/courses/integration/event/SendCourseToApproveIntegrationEvent.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.integration.event; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.UUID; 6 | 7 | /** 8 | * Represents send course to approve integration event. 9 | */ 10 | @Getter 11 | public class SendCourseToApproveIntegrationEvent { 12 | 13 | private final UUID courseId; 14 | 15 | 16 | /** 17 | * Create a new {@code SendCourseToApproveIntegrationEvent}. 18 | * 19 | * @param courseId course id 20 | */ 21 | public SendCourseToApproveIntegrationEvent(UUID courseId) { 22 | this.courseId = courseId; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/student/StudentRepository.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.student; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | /** 6 | * Represents student repository. 7 | */ 8 | public interface StudentRepository extends JpaRepository { 9 | 10 | /** 11 | * Retrieves a student by its username. 12 | * 13 | * @param username must not be {@literal null}. 14 | * @return the student with the given username. 15 | * @throws IllegalArgumentException if {@literal username} is {@literal null}. 16 | */ 17 | Student findByUsername(String username); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/Lecture.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import jakarta.persistence.DiscriminatorValue; 4 | import jakarta.persistence.Entity; 5 | 6 | import com.educational.platform.courses.course.create.CreateLectureCommand; 7 | 8 | @Entity 9 | @DiscriminatorValue("Lecture") 10 | public class Lecture extends CurriculumItem { 11 | 12 | private String content; 13 | 14 | // for JPA 15 | private Lecture() { 16 | super(); 17 | } 18 | 19 | public Lecture(CreateLectureCommand command, Integer serialNumber, Course course) { 20 | super(command.getTitle(), command.getDescription(), course, serialNumber); 21 | this.content = command.getText(); 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0003-cqrs.md: -------------------------------------------------------------------------------- 1 | # 3. CQRS. 2 | Date: 2020-03-22 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | There are possible solutions for writing and reading operations: 9 | - use the same model; 10 | - use separate models; 11 | 12 | ## Decision 13 | CQRS principle will be used. It gives the flexibility in optimizing model for read and write operations. The simple version of CQRS is implemented in this application. On write operations, full logic is executed via aggregate. On read operations, DTO objects are created via JPQL queries on repository level. 14 | - https://dzone.com/articles/cqrs-understanding-from-first-principles 15 | - https://stackoverflow.com/questions/24474859/what-is-the-difference-between-command-commandhandler-and-service 16 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/CourseProposalAlreadyDeclinedException.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * Represents course proposal already declined exception. 7 | */ 8 | public class CourseProposalAlreadyDeclinedException extends RuntimeException { 9 | 10 | private final UUID uuid; 11 | 12 | public CourseProposalAlreadyDeclinedException(UUID uuid) { 13 | super(); 14 | this.uuid = uuid; 15 | } 16 | 17 | @Override 18 | public String getMessage() { 19 | return "Course Proposal with uuid = " + uuid + " cannot be declined, course proposal was already declined"; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /administration/integration-events/src/main/java/com/educational/platform/administration/integration/event/CourseDeclinedByAdminIntegrationEvent.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.integration.event; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.UUID; 6 | 7 | /** 8 | * Represents course declined by admin integration event. 9 | */ 10 | @Getter 11 | public class CourseDeclinedByAdminIntegrationEvent { 12 | 13 | 14 | private final UUID courseId; 15 | 16 | 17 | /** 18 | * Create a new {@code CourseDeclinedByAdminIntegrationEvent}. 19 | * 20 | * @param courseId course id 21 | */ 22 | public CourseDeclinedByAdminIntegrationEvent(UUID courseId) { 23 | this.courseId = courseId; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CourseCannotBePublishedException.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * Course cannot be published exception, can be thrown when course is not approved in the case of publish action 7 | */ 8 | public class CourseCannotBePublishedException extends RuntimeException { 9 | 10 | private final UUID uuid; 11 | 12 | public CourseCannotBePublishedException(UUID uuid) { 13 | super(); 14 | this.uuid = uuid; 15 | } 16 | 17 | @Override 18 | public String getMessage() { 19 | return "Course with uuid = " + uuid + " cannot be published, course should be approved by admin at first"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/CourseReviewChecker.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.stereotype.Component; 7 | 8 | import lombok.RequiredArgsConstructor; 9 | 10 | /** 11 | * Represents the logic for checking if user is a reviewer of course review 12 | */ 13 | @RequiredArgsConstructor 14 | @Component 15 | public class CourseReviewChecker { 16 | 17 | private final CourseReviewRepository courseReviewRepository; 18 | 19 | public boolean hasAccess(Authentication authentication, UUID reviewId) { 20 | return courseReviewRepository.isReviewer(reviewId, authentication.getName()); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /course-reviews/web/src/main/java/com/educational/platform/course/reviews/UpdateCourseReviewRequest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import jakarta.validation.constraints.Max; 9 | import jakarta.validation.constraints.NotNull; 10 | import jakarta.validation.constraints.PositiveOrZero; 11 | 12 | /** 13 | * Represents update course review request. 14 | */ 15 | @Builder 16 | @Data 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | public class UpdateCourseReviewRequest { 20 | 21 | @Max(5) 22 | @PositiveOrZero 23 | @NotNull 24 | private Double rating; 25 | private String comment; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/CourseProposalAlreadyApprovedException.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * Course proposal already approved exception because course proposal is already approved. 7 | */ 8 | public class CourseProposalAlreadyApprovedException extends RuntimeException { 9 | 10 | private final UUID uuid; 11 | 12 | public CourseProposalAlreadyApprovedException(UUID uuid) { 13 | super(); 14 | this.uuid = uuid; 15 | } 16 | 17 | @Override 18 | public String getMessage() { 19 | return "Course Proposal with uuid = " + uuid + " cannot be approved, course proposal was already approved"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /course-reviews/web/src/main/java/com/educational/platform/course/reviews/ReviewCourseRequest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import jakarta.validation.constraints.Max; 9 | import jakarta.validation.constraints.NotNull; 10 | import jakarta.validation.constraints.PositiveOrZero; 11 | import java.util.UUID; 12 | 13 | /** 14 | * Represents review course request. 15 | */ 16 | @Builder 17 | @Data 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | public class ReviewCourseRequest { 21 | 22 | @Max(5) 23 | @PositiveOrZero 24 | @NotNull 25 | private Double rating; 26 | private String comment; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /course-reviews/application/src/test/resources/course_review.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM course_review; 2 | DELETE FROM reviewer; 3 | DELETE FROM reviewable_course; 4 | 5 | INSERT INTO reviewer (username) VALUES ('reviewer'); 6 | INSERT INTO reviewable_course (original_course_id) VALUES ('123E4567E89B12D3A456426655440000'); 7 | INSERT INTO course_review (uuid, rating, comment) VALUES ('123E4567E89B12D3A456426655440001', 4, 'comment'); 8 | UPDATE course_review SET reviewer = (SELECT reviewer.id FROM reviewer WHERE reviewer.username = 'reviewer' GROUP BY reviewer.id); 9 | UPDATE course_review SET course = (SELECT reviewable_course.id FROM reviewable_course WHERE reviewable_course.original_course_id = '123E4567E89B12D3A456426655440000' GROUP BY reviewable_course.id); 10 | 11 | INSERT INTO reviewer (username) VALUES ('another-reviewer'); 12 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/create/ReviewCourseCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.create; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import jakarta.validation.constraints.Max; 8 | import jakarta.validation.constraints.NotNull; 9 | import jakarta.validation.constraints.PositiveOrZero; 10 | import java.util.UUID; 11 | 12 | /** 13 | * Review Course Command. 14 | */ 15 | @Builder 16 | @Data 17 | @AllArgsConstructor 18 | public class ReviewCourseCommand { 19 | 20 | @NotNull 21 | private final UUID courseId; 22 | 23 | @Max(5) 24 | @PositiveOrZero 25 | @NotNull 26 | private final Double rating; 27 | private final String comment; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /administration/integration-events/src/main/java/com/educational/platform/administration/integration/event/CourseApprovedByAdminIntegrationEvent.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.integration.event; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.UUID; 6 | 7 | /** 8 | * Represents course approved by admin integration event, should be published after approval the course by admin. 9 | */ 10 | @Getter 11 | public class CourseApprovedByAdminIntegrationEvent { 12 | 13 | 14 | private final UUID courseId; 15 | 16 | 17 | /** 18 | * Create a new {@code CourseApprovedByAdminIntegrationEvent}. 19 | * 20 | * @param courseId course id 21 | */ 22 | public CourseApprovedByAdminIntegrationEvent(UUID courseId) { 23 | this.courseId = courseId; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/edit/UpdateCourseReviewCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.edit; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | import jakarta.validation.constraints.Max; 8 | import jakarta.validation.constraints.NotNull; 9 | import jakarta.validation.constraints.PositiveOrZero; 10 | import java.util.UUID; 11 | 12 | /** 13 | * Update Course Review Command. 14 | */ 15 | @Builder 16 | @Data 17 | @AllArgsConstructor 18 | public class UpdateCourseReviewCommand { 19 | 20 | @NotNull 21 | private final UUID uuid; 22 | 23 | @Max(5) 24 | @PositiveOrZero 25 | @NotNull 26 | private final Double rating; 27 | private final String comment; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/reviewer/ReviewerRepository.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.reviewer; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import com.educational.platform.course.reviews.reviewer.Reviewer; 6 | 7 | /** 8 | * Represents reviewer repository. 9 | */ 10 | public interface ReviewerRepository extends JpaRepository { 11 | 12 | /** 13 | * Retrieves a reviewer by its username. 14 | * 15 | * @param username must not be {@literal null}. 16 | * @return the reviewer with the given username. 17 | * @throws IllegalArgumentException if {@literal username} is {@literal null}. 18 | */ 19 | Reviewer findByUsername(String username); 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0002-integration-events-implementation.md: -------------------------------------------------------------------------------- 1 | # 2. Integration events. 2 | Date: 2020-03-04 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | For implementing event-driven application, the platform should be available for communication with integration events. 9 | 10 | ## Decision 11 | We will start from standard Spring Events classes: ApplicationListener, ApplicationEvent without dependency to external middleware component. We can add custom features to Spring functionality when it's needed. 12 | todo: For now, all events will be stored in integration-events module. But this solution should be reviewed. Integration events should be published after successful transaction. 13 | - https://devblogs.microsoft.com/cesardelatorre/domain-events-vs-integration-events-in-domain-driven-design-and-microservices-architectures/ 14 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/Question.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.GeneratedValue; 5 | import jakarta.persistence.GenerationType; 6 | import jakarta.persistence.Id; 7 | import jakarta.persistence.JoinColumn; 8 | import jakarta.persistence.ManyToOne; 9 | 10 | @Entity 11 | public class Question { 12 | 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.IDENTITY) 15 | private Integer id; 16 | 17 | private String content; 18 | 19 | @ManyToOne 20 | @JoinColumn(name = "quiz_id") 21 | private Quiz quiz; 22 | 23 | // for JPA 24 | protected Question() { 25 | super(); 26 | } 27 | 28 | public Question(String content, Quiz quiz) { 29 | this.content = content; 30 | this.quiz = quiz; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/publish/CourseTeacherChecker.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.publish; 2 | 3 | import com.educational.platform.courses.course.CourseRepository; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.security.core.Authentication; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.UUID; 9 | 10 | /** 11 | * Represents the logic for checking if user is a teacher of course 12 | */ 13 | @RequiredArgsConstructor 14 | @Component 15 | public class CourseTeacherChecker { 16 | 17 | private final CourseRepository courseRepository; 18 | 19 | public boolean hasAccess(Authentication authentication, UUID courseId) { 20 | return courseRepository.isTeacher(courseId, authentication.getName()); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/CourseEnrollmentDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Value; 6 | 7 | import java.util.UUID; 8 | 9 | /** 10 | * Represents DTO for {@link CourseEnrollment} 11 | */ 12 | @Builder 13 | @AllArgsConstructor 14 | @Value 15 | public class CourseEnrollmentDTO { 16 | 17 | UUID uuid; 18 | UUID course; 19 | String student; 20 | CompletionStatusDTO completionStatus; 21 | 22 | public CourseEnrollmentDTO(UUID uuid, UUID course, String student, CompletionStatus completionStatus) { 23 | this.uuid = uuid; 24 | this.course = course; 25 | this.student = student; 26 | this.completionStatus = completionStatus.toDTO(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /users/web/src/main/java/com/educational/platform/users/security/SignUpRequest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users.security; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import jakarta.validation.constraints.NotBlank; 9 | import jakarta.validation.constraints.NotNull; 10 | 11 | import com.educational.platform.users.RoleDTO; 12 | 13 | /** 14 | * Represents Course Create Request. 15 | */ 16 | @Builder 17 | @Data 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | public class SignUpRequest { 21 | 22 | @NotNull 23 | private RoleDTO role; 24 | 25 | @NotBlank 26 | private String username; 27 | 28 | @NotBlank 29 | private String email; 30 | 31 | @NotBlank 32 | private String password; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/course/EnrollCourseRepository.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.course; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Optional; 6 | import java.util.UUID; 7 | 8 | /** 9 | * Represents course repository. 10 | */ 11 | public interface EnrollCourseRepository extends JpaRepository { 12 | 13 | /** 14 | * Retrieves a course by its uuid. 15 | * 16 | * @param uuid must not be {@literal null}. 17 | * @return the course with the given uuid or {@literal Optional#empty()} if none found. 18 | * @throws IllegalArgumentException if {@literal uuid} is {@literal null}. 19 | */ 20 | Optional findByUuid(UUID uuid); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /configuration/src/main/java/com/educational/platform/AxonConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform; 2 | 3 | import org.axonframework.eventhandling.tokenstore.TokenStore; 4 | import org.axonframework.eventhandling.tokenstore.inmemory.InMemoryTokenStore; 5 | import org.axonframework.eventsourcing.eventstore.EventStorageEngine; 6 | import org.axonframework.eventsourcing.eventstore.inmemory.InMemoryEventStorageEngine; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | /** 11 | * Represents overrides for axon beans. 12 | */ 13 | @Configuration 14 | public class AxonConfiguration { 15 | 16 | @Bean 17 | public TokenStore tokenStore() { 18 | return new InMemoryTokenStore(); 19 | } 20 | 21 | @Bean 22 | public EventStorageEngine eventStorageEngine() { 23 | return new InMemoryEventStorageEngine(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /docs/bounded_context_map.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | namespace administration { 4 | Administrator "1" -- "many" CourseProposalDecision : is making > 5 | CourseProposalDecision -- CourseProposal : for > 6 | } 7 | 8 | namespace userservice { 9 | User -- administration.Administrator 10 | } 11 | 12 | namespace courseservice { 13 | Teacher -- Course : submits for review > 14 | Course -- administration.CourseProposal : available for enrollment after approve 15 | Teacher -- userservice.User 16 | } 17 | 18 | namespace courseenrollmentsservice { 19 | Student -- CourseEnrollment : enrolls course > 20 | CourseEnrollment -- courseservice.Course : for > 21 | Student -- userservice.User 22 | } 23 | 24 | namespace coursereviewsservice { 25 | Student -- CourseReview : is making feedback > 26 | CourseReview -- courseenrollmentsservice.CourseEnrollment : for > 27 | Student -- userservice.User 28 | } 29 | 30 | @enduml 31 | -------------------------------------------------------------------------------- /security/config/src/main/java/com/educational/platform/security/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.security; 2 | 3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.security.crypto.password.PasswordEncoder; 9 | 10 | /** 11 | * Represents security settings. 12 | */ 13 | @Configuration 14 | @EnableMethodSecurity 15 | @ConditionalOnProperty(name = "com.educational.platform.security.enabled", havingValue = "true") 16 | public class SecurityConfiguration { 17 | 18 | @Bean 19 | public PasswordEncoder passwordEncoder() { 20 | return new BCryptPasswordEncoder(12); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /configuration/src/test/java/com/educational/platform/layer/LayerTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.layer; 2 | 3 | import com.tngtech.archunit.junit.AnalyzeClasses; 4 | import com.tngtech.archunit.junit.ArchTest; 5 | import com.tngtech.archunit.lang.ArchRule; 6 | import org.springframework.stereotype.Repository; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; 10 | 11 | @AnalyzeClasses(packages = "com.educational.platform") 12 | public class LayerTest { 13 | 14 | @ArchTest 15 | public static final ArchRule controllers_mustNotAccess_repositories = noClasses() 16 | .that().areAnnotatedWith(RestController.class) 17 | .should().accessClassesThat().areAssignableFrom(Repository.class) 18 | .because("Controllers should not contain repository calls, all logic should be executed via command/query handlers."); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/course/ReviewableCourseRepository.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.course; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Optional; 6 | import java.util.UUID; 7 | 8 | import com.educational.platform.course.reviews.course.ReviewableCourse; 9 | 10 | /** 11 | * Represents reviewable course repository. 12 | */ 13 | public interface ReviewableCourseRepository extends JpaRepository { 14 | 15 | /** 16 | * Retrieves a course by its uuid. 17 | * 18 | * @param uuid must not be {@literal null}. 19 | * @return the course with the given uuid or {@literal Optional#empty()} if none found. 20 | * @throws IllegalArgumentException if {@literal uuid} is {@literal null}. 21 | */ 22 | Optional findByOriginalCourseId(UUID uuid); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/teacher/create/CreateTeacherCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.teacher.create; 2 | 3 | import com.educational.platform.courses.teacher.Teacher; 4 | import com.educational.platform.courses.teacher.TeacherRepository; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import org.axonframework.commandhandling.CommandHandler; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | /** 12 | * Command handler for {@link CreateTeacherCommand} creates a teacher. 13 | */ 14 | @RequiredArgsConstructor 15 | @Component 16 | @Transactional 17 | public class CreateTeacherCommandHandler { 18 | 19 | private final TeacherRepository teacherRepository; 20 | 21 | @CommandHandler 22 | public void handle(CreateTeacherCommand command) { 23 | teacherRepository.save(new Teacher(command)); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /course-enrollments/integration-events/src/main/java/com/educational/platform/course/enrollments/integration/event/StudentEnrolledToCourseIntegrationEvent.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.integration.event; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.UUID; 6 | 7 | /** 8 | * Represents student enrolled to course integration event, should be published after enrollment to course by student. 9 | */ 10 | @Getter 11 | public class StudentEnrolledToCourseIntegrationEvent { 12 | 13 | 14 | private final UUID courseId; 15 | private final String username; 16 | 17 | 18 | /** 19 | * Create a new {@code StudentEnrolledToCourseIntegrationEvent}. 20 | * 21 | * @param courseId course id 22 | * @param username student username 23 | */ 24 | public StudentEnrolledToCourseIntegrationEvent(UUID courseId, String username) { 25 | this.courseId = courseId; 26 | this.username = username; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /users/application/src/main/java/com/educational/platform/users/ValidPassword.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users; 2 | 3 | 4 | import jakarta.validation.Constraint; 5 | import jakarta.validation.Payload; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.Target; 10 | 11 | import static java.lang.annotation.ElementType.*; 12 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 13 | 14 | /** 15 | * The annotated element must match the rules for password defined in {@link PasswordConstraintValidator}. 16 | */ 17 | @Documented 18 | @Constraint(validatedBy = PasswordConstraintValidator.class) 19 | @Target({ TYPE, FIELD, ANNOTATION_TYPE }) 20 | @Retention(RUNTIME) 21 | public @interface ValidPassword { 22 | 23 | String message() default "Invalid Password"; 24 | 25 | Class[] groups() default {}; 26 | 27 | Class[] payload() default {}; 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/teacher/create/UserCreatedIntegrationEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.teacher.create; 2 | 3 | import com.educational.platform.users.integration.event.UserCreatedIntegrationEvent; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import org.axonframework.commandhandling.gateway.CommandGateway; 7 | import org.axonframework.eventhandling.EventHandler; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * Event listener for {@link UserCreatedIntegrationEvent}. 12 | */ 13 | // todo should be transactional? 14 | @Component 15 | @RequiredArgsConstructor 16 | public class UserCreatedIntegrationEventHandler { 17 | 18 | private final CommandGateway commandGateway; 19 | 20 | @EventHandler 21 | public void handleUserCreatedEvent(UserCreatedIntegrationEvent event) { 22 | commandGateway.send(new CreateTeacherCommand(event.getUsername())); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/approve/CourseApprovedByAdminIntegrationEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.approve; 2 | 3 | import com.educational.platform.administration.integration.event.CourseApprovedByAdminIntegrationEvent; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import org.axonframework.commandhandling.gateway.CommandGateway; 7 | import org.axonframework.eventhandling.EventHandler; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * Event listener for {@link CourseApprovedByAdminIntegrationEvent}. 12 | */ 13 | @Component 14 | @RequiredArgsConstructor 15 | public class CourseApprovedByAdminIntegrationEventHandler { 16 | 17 | private final CommandGateway commandGateway; 18 | 19 | @EventHandler 20 | public void handleCourseApprovedByAdminEvent(CourseApprovedByAdminIntegrationEvent event) { 21 | commandGateway.send(new ApproveCourseCommand(event.getCourseId())); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /users/application/src/main/java/com/educational/platform/users/Role.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | 5 | /** 6 | * Represents possible roles in the system. 7 | */ 8 | public enum Role implements GrantedAuthority { 9 | 10 | ROLE_ADMIN, 11 | ROLE_STUDENT, 12 | ROLE_TEACHER; 13 | 14 | public static Role from(RoleDTO role) { 15 | switch (role) { 16 | case ROLE_STUDENT: 17 | return ROLE_STUDENT; 18 | case ROLE_TEACHER: 19 | return ROLE_TEACHER; 20 | } 21 | 22 | return null; 23 | } 24 | 25 | public String getAuthority() { 26 | return name(); 27 | } 28 | 29 | public RoleDTO toDTO() { 30 | switch (this) { 31 | case ROLE_STUDENT: 32 | return RoleDTO.ROLE_STUDENT; 33 | case ROLE_TEACHER: 34 | return RoleDTO.ROLE_TEACHER; 35 | } 36 | 37 | return null; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /users/application/src/main/java/com/educational/platform/users/registration/UserRegistrationCommand.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users.registration; 2 | 3 | import com.educational.platform.users.RoleDTO; 4 | import com.educational.platform.users.ValidPassword; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | 9 | import jakarta.validation.constraints.Email; 10 | import jakarta.validation.constraints.NotBlank; 11 | import jakarta.validation.constraints.NotNull; 12 | import jakarta.validation.constraints.Size; 13 | 14 | /** 15 | * Represents User Registration Command. 16 | */ 17 | @Builder 18 | @Data 19 | @AllArgsConstructor 20 | public class UserRegistrationCommand { 21 | 22 | @NotNull 23 | private final RoleDTO role; 24 | 25 | @Size(min = 4, max = 255) 26 | @NotBlank 27 | private final String username; 28 | 29 | @Email 30 | @NotBlank 31 | private final String email; 32 | 33 | @ValidPassword 34 | @NotBlank 35 | private final String password; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/query/ListCourseQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.query; 2 | 3 | import java.util.List; 4 | 5 | import org.axonframework.queryhandling.QueryHandler; 6 | import org.springframework.lang.NonNull; 7 | import org.springframework.stereotype.Component; 8 | 9 | import com.educational.platform.courses.course.CourseLightDTO; 10 | import com.educational.platform.courses.course.CourseRepository; 11 | 12 | import lombok.RequiredArgsConstructor; 13 | 14 | /** 15 | * Query handler for getting the list of courses. 16 | */ 17 | @RequiredArgsConstructor 18 | @Component 19 | public class ListCourseQueryHandler { 20 | 21 | private final CourseRepository repository; 22 | 23 | /** 24 | * Retrieves list of course dtos. 25 | * 26 | * @param query query. 27 | * @return corresponding list of course dtos. 28 | */ 29 | @QueryHandler 30 | @NonNull 31 | public List handle(ListCourseQuery query) { 32 | return repository.list(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/query/CourseByUUIDQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.query; 2 | 3 | import java.util.Optional; 4 | 5 | import org.axonframework.queryhandling.QueryHandler; 6 | import org.springframework.lang.NonNull; 7 | import org.springframework.stereotype.Component; 8 | 9 | import com.educational.platform.courses.course.CourseDTO; 10 | import com.educational.platform.courses.course.CourseRepository; 11 | 12 | import lombok.RequiredArgsConstructor; 13 | 14 | /** 15 | * Query handler for getting the course by uuid. 16 | */ 17 | @RequiredArgsConstructor 18 | @Component 19 | public class CourseByUUIDQueryHandler { 20 | 21 | private final CourseRepository repository; 22 | 23 | /** 24 | * Retrieves course by uuid. 25 | * 26 | * @param query query. 27 | * @return corresponding course dto. 28 | */ 29 | @QueryHandler 30 | @NonNull 31 | public Optional handle(CourseByUUIDQuery query) { 32 | return repository.findDTOByUuid(query.getUuid()); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /users/web/src/main/java/com/educational/platform/users/security/JwtTokenFilterConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users.security; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.security.config.annotation.SecurityConfigurerAdapter; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.web.DefaultSecurityFilterChain; 7 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 8 | 9 | /** 10 | * Represents Jwt Token filter configurer. 11 | */ 12 | @RequiredArgsConstructor 13 | public class JwtTokenFilterConfigurer extends SecurityConfigurerAdapter { 14 | 15 | private final JwtTokenProvider jwtTokenProvider; 16 | 17 | @Override 18 | public void configure(HttpSecurity http) { 19 | final JwtTokenFilter jwtTokenFilter = new JwtTokenFilter(jwtTokenProvider); 20 | http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/rating/update/CourseRatingRecalculatedIntegrationEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.rating.update; 2 | 3 | import com.educational.platform.course.reviews.integration.event.CourseRatingRecalculatedIntegrationEvent; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import org.axonframework.commandhandling.gateway.CommandGateway; 7 | import org.axonframework.eventhandling.EventHandler; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * Event listener for {@link CourseRatingRecalculatedIntegrationEvent}. 12 | */ 13 | @Component 14 | @RequiredArgsConstructor 15 | public class CourseRatingRecalculatedIntegrationEventHandler { 16 | 17 | private final CommandGateway commandGateway; 18 | 19 | @EventHandler 20 | public void handleCourseRatingRecalculatedEvent(CourseRatingRecalculatedIntegrationEvent event) { 21 | commandGateway.send(new UpdateCourseRatingCommand(event.getCourseId(), event.getRating())); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/reviewer/Reviewer.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.reviewer; 2 | 3 | import com.educational.platform.common.domain.AggregateRoot; 4 | import com.educational.platform.course.reviews.reviewer.create.CreateReviewerCommand; 5 | 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | 11 | /** 12 | * Represents Reviewer domain model. 13 | */ 14 | @Entity 15 | public class Reviewer implements AggregateRoot { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Integer id; 20 | 21 | private String username; 22 | 23 | // for JPA 24 | private Reviewer() { 25 | } 26 | 27 | public Reviewer(CreateReviewerCommand createReviewerCommand) { 28 | this.username = createReviewerCommand.getUsername(); 29 | } 30 | 31 | public Integer getId() { 32 | return id; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/numberofsudents/update/StudentEnrolledToCourseIntegrationEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.numberofsudents.update; 2 | 3 | import com.educational.platform.course.enrollments.integration.event.StudentEnrolledToCourseIntegrationEvent; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import org.axonframework.commandhandling.gateway.CommandGateway; 7 | import org.axonframework.eventhandling.EventHandler; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * Event listener for {@link StudentEnrolledToCourseIntegrationEvent}. 12 | */ 13 | @Component 14 | @RequiredArgsConstructor 15 | public class StudentEnrolledToCourseIntegrationEventHandler { 16 | 17 | private final CommandGateway commandGateway; 18 | 19 | @EventHandler 20 | public void handleStudentEnrolledToCourseEvent(StudentEnrolledToCourseIntegrationEvent event) { 21 | commandGateway.send(new IncreaseNumberOfStudentsCommand(event.getCourseId())); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /administration/application/src/main/resources/db/administration.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - changeSet: 3 | id: 2021_06_13-1 4 | author: antonliauchuk 5 | preConditions: 6 | onFail: MARK_RAN 7 | not: 8 | tableExists: 9 | tableName: course_proposal 10 | changes: 11 | - createTable: 12 | tableName: course_proposal 13 | columns: 14 | - column: 15 | name: id 16 | type: int 17 | autoIncrement: true 18 | startWith: 1 19 | incrementBy: 1 20 | constraints: 21 | primaryKey: true 22 | primaryKeyName: course_proposal_pk 23 | - column: 24 | constraints: 25 | nullable: false 26 | name: uuid 27 | type: uuid 28 | - column: 29 | constraints: 30 | nullable: false 31 | name: status 32 | type: VARCHAR(14) 33 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/create/SendCourseToApproveIntegrationEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.create; 2 | 3 | import com.educational.platform.courses.integration.event.SendCourseToApproveIntegrationEvent; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import org.axonframework.commandhandling.gateway.CommandGateway; 7 | import org.axonframework.eventhandling.EventHandler; 8 | import jakarta.inject.Named; 9 | 10 | /** 11 | * Event listener for {@link SendCourseToApproveIntegrationEvent}, executes the logic for creating course proposal by {@link CreateCourseProposalCommandHandler}. 12 | */ 13 | @Named 14 | @RequiredArgsConstructor 15 | public class SendCourseToApproveIntegrationEventHandler { 16 | 17 | private final CommandGateway commandGateway; 18 | 19 | @EventHandler 20 | public void handleSendCourseToApproveEvent(SendCourseToApproveIntegrationEvent event) { 21 | commandGateway.send(new CreateCourseProposalCommand(event.getCourseId())); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/teacher/Teacher.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.teacher; 2 | 3 | import com.educational.platform.common.domain.AggregateRoot; 4 | import com.educational.platform.courses.teacher.create.CreateTeacherCommand; 5 | 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | 11 | /** 12 | * Represents Teacher model. 13 | */ 14 | @Entity 15 | public class Teacher implements AggregateRoot { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Integer id; 20 | 21 | private String username; 22 | 23 | // for JPA 24 | private Teacher() { 25 | 26 | } 27 | 28 | public Teacher(CreateTeacherCommand command) { 29 | this.username = command.getUsername(); 30 | } 31 | 32 | public Integer getId() { 33 | return id; 34 | } 35 | 36 | public String toIdentity() { 37 | return username; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anton Liauchuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/Quiz.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import jakarta.persistence.CascadeType; 7 | import jakarta.persistence.DiscriminatorValue; 8 | import jakarta.persistence.Entity; 9 | import jakarta.persistence.FetchType; 10 | import jakarta.persistence.OneToMany; 11 | 12 | import com.educational.platform.courses.course.create.CreateQuizCommand; 13 | 14 | @Entity 15 | @DiscriminatorValue("Quiz") 16 | public class Quiz extends CurriculumItem { 17 | 18 | @OneToMany(mappedBy = "quiz", cascade = CascadeType.ALL, fetch = FetchType.EAGER) 19 | private List questions; 20 | 21 | // for JPA 22 | protected Quiz() { 23 | super(); 24 | } 25 | 26 | public Quiz(CreateQuizCommand command, Integer serialNumber, Course course) { 27 | super(command.getTitle(), command.getDescription(), course, serialNumber); 28 | this.questions = command.getQuestions().stream().map(questionCommand -> new Question(questionCommand.getContent(), this)).collect(Collectors.toList()); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /users/application/src/main/java/com/educational/platform/users/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * Represents User repository. 9 | */ 10 | public interface UserRepository extends JpaRepository { 11 | 12 | /** 13 | * Returns whether an user with the given username exists. 14 | * 15 | * @param username must not be {@literal null}. 16 | * @return {@literal true} if an user with the given username exists, {@literal false} otherwise. 17 | * @throws IllegalArgumentException if {@literal username} is {@literal null}. 18 | */ 19 | boolean existsByUsername(String username); 20 | 21 | /** 22 | * Retrieves an user by its username. 23 | * 24 | * @param username must not be {@literal null}. 25 | * @return the user with the given username or {@literal Optional#empty()} if none found. 26 | * @throws IllegalArgumentException if {@literal username} is {@literal null}. 27 | */ 28 | Optional findByUsername(String username); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/query/ListCourseReviewsByCourseUUIDQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.query; 2 | 3 | import java.util.List; 4 | 5 | import org.axonframework.queryhandling.QueryHandler; 6 | import org.springframework.lang.NonNull; 7 | import org.springframework.stereotype.Component; 8 | 9 | import com.educational.platform.course.reviews.CourseReviewDTO; 10 | import com.educational.platform.course.reviews.CourseReviewRepository; 11 | 12 | import lombok.RequiredArgsConstructor; 13 | 14 | /** 15 | * Query handler for getting the course reviews by course uuid. 16 | */ 17 | @RequiredArgsConstructor 18 | @Component 19 | public class ListCourseReviewsByCourseUUIDQueryHandler { 20 | 21 | private final CourseReviewRepository repository; 22 | 23 | /** 24 | * Retrieves course reviews for particular course uuid. 25 | * 26 | * @param query query. 27 | * @return course reviews. 28 | */ 29 | @QueryHandler 30 | @NonNull 31 | public List handle(ListCourseReviewsByCourseUUIDQuery query) { 32 | return repository.listCourseReviews(query.getUuid()); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/student/Student.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.student; 2 | 3 | import com.educational.platform.common.domain.AggregateRoot; 4 | import com.educational.platform.course.enrollments.student.create.CreateStudentCommand; 5 | 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | 11 | /** 12 | * Represents student domain model. 13 | */ 14 | @Entity 15 | public class Student implements AggregateRoot { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Integer id; 20 | 21 | private String username; 22 | 23 | // for JPA 24 | private Student() { 25 | } 26 | 27 | public Student(CreateStudentCommand createStudentCommand) { 28 | this.username = createStudentCommand.getUsername(); 29 | } 30 | 31 | public Integer getId() { 32 | return id; 33 | } 34 | 35 | public String toReference() { 36 | return username; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/course/ReviewableCourse.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.course; 2 | 3 | import com.educational.platform.common.domain.AggregateRoot; 4 | import com.educational.platform.course.reviews.course.create.CreateReviewableCourseCommand; 5 | 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | import java.util.UUID; 11 | 12 | /** 13 | * Represents Reviewable Course domain model. 14 | */ 15 | @Entity 16 | public class ReviewableCourse implements AggregateRoot { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Integer id; 21 | 22 | private UUID originalCourseId; 23 | 24 | // for JPA 25 | private ReviewableCourse() { 26 | } 27 | 28 | public ReviewableCourse(CreateReviewableCourseCommand createReviewableCourseCommand) { 29 | this.originalCourseId = createReviewableCourseCommand.getUuid(); 30 | } 31 | 32 | public Integer getId() { 33 | return id; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /administration/application/src/test/java/com/educational/platform/administration/course/jpa/CourseProposalRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.jpa; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.UUID; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 10 | 11 | import com.educational.platform.administration.course.CourseProposal; 12 | import com.educational.platform.administration.course.CourseProposalRepository; 13 | import com.educational.platform.administration.course.create.CreateCourseProposalCommand; 14 | 15 | @DataJpaTest 16 | public class CourseProposalRepositoryTest { 17 | 18 | @Autowired 19 | private CourseProposalRepository sut; 20 | 21 | @Test 22 | void listCourseProposals_unpaged_courseProposals() { 23 | // given 24 | sut.save(new CourseProposal(new CreateCourseProposalCommand(UUID.fromString("123e4567-e89b-12d3-a456-426655440001")))); 25 | 26 | // when 27 | var result = sut.listCourseProposals(); 28 | 29 | // then 30 | assertThat(result).hasSize(1); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/student/create/CreateStudentCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.student.create; 2 | 3 | import com.educational.platform.course.enrollments.student.Student; 4 | import com.educational.platform.course.enrollments.student.StudentRepository; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import org.axonframework.commandhandling.CommandHandler; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | /** 12 | * Command handler for {@link CreateStudentCommand} creates a student. 13 | */ 14 | @RequiredArgsConstructor 15 | @Component 16 | @Transactional 17 | public class CreateStudentCommandHandler { 18 | 19 | private final StudentRepository studentRepository; 20 | 21 | /** 22 | * Creates student from command. 23 | * 24 | * @param command command 25 | */ 26 | @CommandHandler 27 | public void handle(CreateStudentCommand command) { 28 | final Student student = new Student(command); 29 | studentRepository.save(student); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/reviewer/create/CreateReviewerCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.reviewer.create; 2 | 3 | import com.educational.platform.course.reviews.reviewer.Reviewer; 4 | import com.educational.platform.course.reviews.reviewer.ReviewerRepository; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import org.axonframework.commandhandling.CommandHandler; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | /** 12 | * Command handler for {@link CreateReviewerCommand} creates a reviewer. 13 | */ 14 | @RequiredArgsConstructor 15 | @Component 16 | @Transactional 17 | public class CreateReviewerCommandHandler { 18 | 19 | private final ReviewerRepository reviewerRepository; 20 | 21 | /** 22 | * Creates reviewer from command. 23 | * 24 | * @param command command 25 | */ 26 | @CommandHandler 27 | public void handle(CreateReviewerCommand command) { 28 | final Reviewer reviewer = new Reviewer(command); 29 | reviewerRepository.save(reviewer); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /course-reviews/application/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation(project(":security:security-config")) 4 | 5 | implementation("org.springframework.boot", "spring-boot-starter-security") 6 | implementation("org.springframework.boot", "spring-boot-starter-data-jpa") 7 | implementation("org.springframework.boot", "spring-boot-starter-validation") 8 | 9 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 10 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 11 | 12 | testImplementation("org.springframework.boot", "spring-boot-starter-test") 13 | testImplementation("org.springframework.security", "spring-security-test") 14 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 15 | testImplementation("org.junit.platform", "junit-platform-engine") 16 | testImplementation("org.junit.platform", "junit-platform-launcher") 17 | testImplementation("org.mockito", "mockito-junit-jupiter", libs.versions.mockito.get()) 18 | testImplementation("org.assertj", "assertj-core", libs.versions.assertj.get()) 19 | } 20 | 21 | tasks.test { 22 | useJUnitPlatform() 23 | } 24 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CurrentUserAsTeacher.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import com.educational.platform.courses.teacher.Teacher; 4 | import com.educational.platform.courses.teacher.TeacherRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.security.core.context.SecurityContextHolder; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * Represents the logic for retrieving the teacher entity from database for current authenticated user. 12 | */ 13 | @RequiredArgsConstructor 14 | @Component 15 | public class CurrentUserAsTeacher { 16 | 17 | private final TeacherRepository teacherRepository; 18 | 19 | /** 20 | * Represents current user as teacher. 21 | * 22 | * @return teacher. 23 | */ 24 | public Teacher userAsTeacher() { 25 | var principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 26 | var username = principal.getUsername(); 27 | 28 | return teacherRepository.findByUsername(username); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/query/ListCourseProposalsQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.query; 2 | 3 | import java.util.List; 4 | 5 | import org.axonframework.queryhandling.QueryHandler; 6 | import org.springframework.lang.NonNull; 7 | import org.springframework.security.access.prepost.PreAuthorize; 8 | import jakarta.inject.Named; 9 | 10 | import com.educational.platform.administration.course.CourseProposalDTO; 11 | import com.educational.platform.administration.course.CourseProposalRepository; 12 | 13 | import lombok.RequiredArgsConstructor; 14 | 15 | /** 16 | * Query handler for getting the course proposals by uuid. 17 | */ 18 | @RequiredArgsConstructor 19 | @Named 20 | public class ListCourseProposalsQueryHandler { 21 | 22 | private final CourseProposalRepository repository; 23 | 24 | /** 25 | * Retrieves course proposals. 26 | * 27 | * @param query query. 28 | * @return course proposals. 29 | */ 30 | @QueryHandler 31 | @PreAuthorize("hasRole('ADMIN')") 32 | @NonNull 33 | public List handle(ListCourseProposalsQuery query) { 34 | return repository.listCourseProposals(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/course/EnrollCourse.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.course; 2 | 3 | import com.educational.platform.common.domain.AggregateRoot; 4 | import com.educational.platform.course.enrollments.course.create.CreateCourseCommand; 5 | 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | import java.util.UUID; 11 | 12 | /** 13 | * Represents course domain model. 14 | */ 15 | @Entity(name = "enroll_course") 16 | public class EnrollCourse implements AggregateRoot { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Integer id; 21 | 22 | private UUID uuid; 23 | 24 | // for JPA 25 | private EnrollCourse() { 26 | 27 | } 28 | 29 | public EnrollCourse(CreateCourseCommand createCourseCommand) { 30 | this.uuid = createCourseCommand.getUuid(); 31 | } 32 | 33 | public Integer getId() { 34 | return id; 35 | } 36 | 37 | public UUID toReference() { 38 | return uuid; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/course/create/CreateEnrollmentCourseCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.course.create; 2 | 3 | import com.educational.platform.course.enrollments.course.EnrollCourse; 4 | import com.educational.platform.course.enrollments.course.EnrollCourseRepository; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import org.axonframework.commandhandling.CommandHandler; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | /** 12 | * Command handler for {@link CreateCourseCommand} creates a course. 13 | */ 14 | @RequiredArgsConstructor 15 | @Component 16 | @Transactional 17 | public class CreateEnrollmentCourseCommandHandler { 18 | 19 | private final EnrollCourseRepository courseRepository; 20 | 21 | /** 22 | * Creates course from command. 23 | * 24 | * @param command command 25 | */ 26 | @CommandHandler 27 | public void handle(CreateCourseCommand command) { 28 | final EnrollCourse course = new EnrollCourse(command); 29 | courseRepository.save(course); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /users/application/src/main/java/com/educational/platform/users/security/MyUserDetails.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users.security; 2 | 3 | import com.educational.platform.users.User; 4 | import com.educational.platform.users.UserRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.core.userdetails.UserDetailsService; 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.Optional; 12 | 13 | /** 14 | * Represents user details implementation. 15 | */ 16 | @RequiredArgsConstructor 17 | @Service 18 | public class MyUserDetails implements UserDetailsService { 19 | 20 | private final UserRepository userRepository; 21 | 22 | @Override 23 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 24 | final Optional user = userRepository.findByUsername(username); 25 | 26 | return user 27 | .map(User::toUserDetails) 28 | .orElseThrow(() -> new UsernameNotFoundException("User '" + username + "' not found")); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/CurrentUserAsReviewer.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.stereotype.Component; 7 | 8 | import com.educational.platform.course.reviews.reviewer.Reviewer; 9 | import com.educational.platform.course.reviews.reviewer.ReviewerRepository; 10 | 11 | /** 12 | * Represents the logic for retrieving the reviewer entity from database for current authenticated user. 13 | */ 14 | @RequiredArgsConstructor 15 | @Component 16 | public class CurrentUserAsReviewer { 17 | 18 | private final ReviewerRepository reviewerRepository; 19 | 20 | /** 21 | * Represents current user as reviewer. 22 | * 23 | * @return teacher. 24 | */ 25 | public Reviewer userAsReviewer() { 26 | var principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 27 | var username = principal.getUsername(); 28 | 29 | return reviewerRepository.findByUsername(username); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/CurrentUserAsStudent.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.stereotype.Component; 7 | 8 | import com.educational.platform.course.enrollments.student.Student; 9 | import com.educational.platform.course.enrollments.student.StudentRepository; 10 | 11 | /** 12 | * Represents the logic for retrieving the student entity from database for current authenticated user. 13 | */ 14 | @RequiredArgsConstructor 15 | @Component 16 | public class CurrentUserAsStudent { 17 | 18 | private final StudentRepository studentRepository; 19 | 20 | /** 21 | * Represents current user as student. 22 | * 23 | * @return teacher. 24 | */ 25 | public Student userAsStudent() { 26 | var principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 27 | var username = principal.getUsername(); 28 | 29 | return studentRepository.findByUsername(username); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0004-validation.md: -------------------------------------------------------------------------------- 1 | # 4. Validation. 2 | Date: 2020-03-25 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | There are possible solutions related to validation topic: 9 | - always valid approach. Domain model is encapsulated, all requests are validated before executing logic in domain model; 10 | - deferred validation. Write all data to domain model from request and validate everything in domain model; 11 | - return validation object. 12 | 13 | ## Decision 14 | Always valid approach will be used. So domain model will be changed from one valid state to another valid state. Technically, validation rules are defined on `Command` models and executed during processing the command. Javax validation-api is used for defining the validation rules via annotations. 15 | 16 | Useful links: 17 | - https://danielwhittaker.me/2016/04/20/how-to-validate-commands-in-a-cqrs-application/ 18 | - https://enterprisecraftsmanship.com/posts/validate-commands-cqrs/ 19 | - https://enterprisecraftsmanship.com/posts/validation-and-ddd/ 20 | - https://enterprisecraftsmanship.com/posts/always-valid-vs-not-always-valid-domain-model/ 21 | - http://www.kamilgrzybek.com/design/domain-model-validation/ 22 | - http://www.kamilgrzybek.com/design/rest-api-data-validation/ 23 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/CourseEnrollment.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments; 2 | 3 | import com.educational.platform.common.domain.AggregateRoot; 4 | 5 | import jakarta.persistence.*; 6 | import java.util.UUID; 7 | 8 | /** 9 | * Represents course enrollment domain model. 10 | */ 11 | @Entity 12 | public class CourseEnrollment implements AggregateRoot { 13 | 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | private Integer id; 17 | 18 | private UUID uuid; 19 | 20 | private Integer course; 21 | 22 | private Integer student; 23 | 24 | private CompletionStatus completionStatus; 25 | 26 | // for JPA 27 | private CourseEnrollment() { 28 | } 29 | 30 | public CourseEnrollment(Integer course, Integer student) { 31 | this.uuid = UUID.randomUUID(); 32 | this.course = course; 33 | this.student = student; 34 | this.completionStatus = CompletionStatus.IN_PROGRESS; 35 | } 36 | 37 | public void complete() { 38 | this.completionStatus = CompletionStatus.COMPLETED; 39 | } 40 | 41 | public UUID getUuid() { 42 | return uuid; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0001-bounded-contexts-communication.md: -------------------------------------------------------------------------------- 1 | # 1. Communications between bounded contexts. 2 | Date: 2020-02-29 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | Some common data should be used by several bounded contexts (modules, in the case of monolith application). 9 | 10 | ## Possible solutions 11 | ### 1. Direct method call 12 | We will create the dependency between modules. After setting up the dependency, needed method can be called from another module. 13 | 14 | ### 2. Event-driven 15 | The modules will not communicate by direct calls, so the dependency between modules is not needed. Common data can be duplicated inside the modules during executing the listener for particular events. 16 | 17 | ## Decision 18 | Communication between bounded contexts asynchronous. Bounded contexts don't share data, it's forbidden to create a transaction which spans more than one bounded context. 19 | - https://www.infoq.com/news/2014/11/sharing-data-bounded-contexts/ 20 | - http://www.kamilgrzybek.com/design/modular-monolith-primer/ 21 | - https://github.com/kgrzybek/modular-monolith-with-ddd#37-modules-integration 22 | 23 | ## Consequences 24 | This solution reduces coupling of bounded contexts through data replication across contexts which results to higher bounded contexts independence. -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/CourseProposalRepository.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | 6 | import java.util.List; 7 | import java.util.Optional; 8 | import java.util.UUID; 9 | 10 | /** 11 | * Represents course proposal repository. 12 | */ 13 | public interface CourseProposalRepository extends JpaRepository { 14 | 15 | /** 16 | * Retrieves a course proposal by its uuid. 17 | * 18 | * @param uuid must not be {@literal null}. 19 | * @return the course proposal with the given uuid or {@literal Optional#empty()} if none found. 20 | * @throws IllegalArgumentException if {@literal uuid} is {@literal null}. 21 | */ 22 | Optional findByUuid(UUID uuid); 23 | 24 | /** 25 | * Retrieves list of course proposals. 26 | * 27 | * @return list of course proposals. 28 | */ 29 | @Query("select new com.educational.platform.administration.course.CourseProposalDTO(cp.uuid, cp.status) from CourseProposal cp") 30 | List listCourseProposals(); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /course-enrollments/application/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation(project(":security:security-config")) 4 | implementation(project(":course-enrollments:course-enrollments-integration-events")) 5 | 6 | implementation("org.springframework.boot", "spring-boot-starter-security") 7 | implementation("org.springframework.boot", "spring-boot-starter-data-jpa") 8 | implementation("org.springframework.boot", "spring-boot-starter-validation") 9 | 10 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 11 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 12 | 13 | testImplementation("org.springframework.boot", "spring-boot-starter-test") 14 | testImplementation("org.springframework.security", "spring-security-test") 15 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 16 | testImplementation("org.junit.platform", "junit-platform-engine") 17 | testImplementation("org.junit.platform", "junit-platform-launcher") 18 | testImplementation("org.mockito", "mockito-junit-jupiter", libs.versions.mockito.get()) 19 | testImplementation("org.assertj", "assertj-core", libs.versions.assertj.get()) 20 | } 21 | 22 | tasks.test { 23 | useJUnitPlatform() 24 | } 25 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/course/create/CreateReviewableCourseCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.course.create; 2 | 3 | import com.educational.platform.course.reviews.course.ReviewableCourse; 4 | import com.educational.platform.course.reviews.course.ReviewableCourseRepository; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import org.axonframework.commandhandling.CommandHandler; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | /** 12 | * Command handler for {@link CreateReviewableCourseCommand} creates a reviewable course. 13 | */ 14 | @RequiredArgsConstructor 15 | @Component 16 | @Transactional 17 | public class CreateReviewableCourseCommandHandler { 18 | 19 | private final ReviewableCourseRepository reviewableCourseRepository; 20 | 21 | /** 22 | * Creates reviewable course from command. 23 | * 24 | * @param command command 25 | */ 26 | @CommandHandler 27 | public void handle(CreateReviewableCourseCommand command) { 28 | final ReviewableCourse reviewableCourse = new ReviewableCourse(command); 29 | reviewableCourseRepository.save(reviewableCourse); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /administration/application/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation(project(":security:security-config")) 4 | implementation(project(":courses:courses-integration-events")) 5 | implementation(project(":administration:administration-integration-events")) 6 | 7 | implementation("org.springframework.boot", "spring-boot-starter-security") 8 | implementation("org.springframework.boot", "spring-boot-starter-data-jpa") 9 | implementation("jakarta.inject", "jakarta.inject-api") 10 | 11 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 12 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 13 | 14 | testImplementation("org.springframework.boot", "spring-boot-starter-test") 15 | testImplementation("org.springframework.security", "spring-security-test") 16 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 17 | testImplementation("org.junit.platform", "junit-platform-engine") 18 | testImplementation("org.junit.platform", "junit-platform-launcher") 19 | testImplementation("org.mockito", "mockito-junit-jupiter", libs.versions.mockito.get()) 20 | testImplementation("org.assertj", "assertj-core", libs.versions.assertj.get()) 21 | } 22 | 23 | tasks.test { 24 | useJUnitPlatform() 25 | } 26 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CourseDTO.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.UUID; 7 | 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Value; 11 | 12 | /** 13 | * Represents course dto. 14 | */ 15 | @Builder 16 | @AllArgsConstructor 17 | @Value 18 | public class CourseDTO { 19 | 20 | public static final String UUID_COLUMN = "course_uuid"; 21 | public static final String NAME_COLUMN = "course_name"; 22 | public static final String DESCRIPTION_COLUMN = "course_description"; 23 | public static final String NUMBER_OF_STUDENTS_COLUMN = "course_numberOfStudents"; 24 | 25 | UUID uuid; 26 | String name, description; 27 | int numberOfStudents; 28 | List curriculumItems; 29 | 30 | public CourseDTO(Object[] tuples, Map aliasToIndexMap) { 31 | this.uuid = (UUID) tuples[aliasToIndexMap.get(UUID_COLUMN)]; 32 | this.name = (String) tuples[aliasToIndexMap.get(NAME_COLUMN)]; 33 | this.description = (String) tuples[aliasToIndexMap.get(DESCRIPTION_COLUMN)]; 34 | this.numberOfStudents = ((NumberOfStudents) tuples[aliasToIndexMap.get(NUMBER_OF_STUDENTS_COLUMN)]).getNumber(); 35 | this.curriculumItems = new ArrayList<>(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/create/CreateCourseProposalCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.create; 2 | 3 | import com.educational.platform.administration.course.CourseProposal; 4 | import com.educational.platform.administration.course.CourseProposalRepository; 5 | 6 | import jakarta.inject.Named; 7 | 8 | import org.axonframework.commandhandling.CommandHandler; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | /** 12 | * Command handler for {@link CreateCourseProposalCommand} creates a course proposal. 13 | */ 14 | @Named 15 | @Transactional 16 | public class CreateCourseProposalCommandHandler { 17 | 18 | private final CourseProposalRepository courseProposalRepository; 19 | 20 | public CreateCourseProposalCommandHandler(CourseProposalRepository courseProposalRepository) { 21 | this.courseProposalRepository = courseProposalRepository; 22 | } 23 | 24 | /** 25 | * Creates course proposal from command. 26 | * 27 | * @param command command 28 | */ 29 | @CommandHandler 30 | public void handle(CreateCourseProposalCommand command) { 31 | final CourseProposal courseProposal = new CourseProposal(command); 32 | courseProposalRepository.save(courseProposal); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /course-enrollments/application/src/test/java/com/educational/platform/course/enrollments/CourseEnrollmentTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.UUID; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import com.educational.platform.course.enrollments.course.EnrollCourse; 10 | import com.educational.platform.course.enrollments.course.create.CreateCourseCommand; 11 | import com.educational.platform.course.enrollments.student.Student; 12 | import com.educational.platform.course.enrollments.student.create.CreateStudentCommand; 13 | 14 | public class CourseEnrollmentTest { 15 | 16 | @Test 17 | void complete_completedStatus() { 18 | // given 19 | final CreateCourseCommand createCourseCommand = new CreateCourseCommand(UUID.fromString("123e4567-e89b-12d3-a456-426655440001")); 20 | final CreateStudentCommand createStudentCommand = new CreateStudentCommand("username"); 21 | final CourseEnrollment enrollment = new CourseEnrollment( 22 | new EnrollCourse(createCourseCommand).getId(), 23 | new Student(createStudentCommand).getId() 24 | ); 25 | 26 | // when 27 | enrollment.complete(); 28 | 29 | // then 30 | assertThat(enrollment).hasFieldOrPropertyWithValue("completionStatus", CompletionStatus.COMPLETED); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CurriculumItemFactory.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import jakarta.validation.ConstraintViolationException; 4 | 5 | import com.educational.platform.courses.course.create.CreateCurriculumItemCommand; 6 | import com.educational.platform.courses.course.create.CreateLectureCommand; 7 | import com.educational.platform.courses.course.create.CreateQuizCommand; 8 | 9 | /** 10 | * Represents Curriculum Item Factory. 11 | */ 12 | public class CurriculumItemFactory { 13 | 14 | /** 15 | * Creates course from command. 16 | * 17 | * @param createCurriculumItemCommand course command 18 | * @return course 19 | * @throws ConstraintViolationException in the case of validation issues 20 | */ 21 | public static CurriculumItem createFrom(CreateCurriculumItemCommand createCurriculumItemCommand, Course course) { 22 | if (createCurriculumItemCommand instanceof CreateLectureCommand) { 23 | return new Lecture((CreateLectureCommand) createCurriculumItemCommand, createCurriculumItemCommand.getSerialNumber(), course); 24 | } else if (createCurriculumItemCommand instanceof CreateQuizCommand) { 25 | return new Quiz((CreateQuizCommand) createCurriculumItemCommand, createCurriculumItemCommand.getSerialNumber(), course); 26 | } 27 | 28 | return null; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /docs/architecture-decisions/0007-api-first.md: -------------------------------------------------------------------------------- 1 | # 7. API First. 2 | Date: 2020-05-05 3 | 4 | ## Status 5 | Accepted 6 | 7 | ## Context 8 | API should have the documentation, we should define the process of delivering API and documentation. 9 | 10 | ## Possible solutions 11 | ### 1. API First 12 | API First is one of engineering and architecture principles. In a nutshell API First requires two aspects: 13 | - define APIs first, before coding its implementation, using a standard specification language; 14 | - get early review feedback from peers and client developers; 15 | 16 | ### 2. Coding First 17 | Another approach - coding first. In this solution, the documentation represented inside the code. And final version of documentation can be shared only after complete implementation of API. 18 | 19 | ## Decision 20 | By defining APIs outside the code, we want to facilitate early review feedback and also a development discipline that focus service interface design on: 21 | - profound understanding of the domain and required functionality 22 | - generalized business entities / resources, i.e. avoidance of use case specific APIs 23 | - clear separation of WHAT vs. HOW concerns, i.e. abstraction from implementation aspects — APIs should be stable even if we replace complete service implementation including its underlying technology stack 24 | 25 | Useful links: 26 | - https://opensource.zalando.com/restful-api-guidelines/#api-first 27 | -------------------------------------------------------------------------------- /users/application/src/main/resources/db/users.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - changeSet: 3 | id: 2021_06_25-1 4 | author: antonliauchuk 5 | preConditions: 6 | onFail: MARK_RAN 7 | not: 8 | tableExists: 9 | tableName: custom_user 10 | changes: 11 | - createTable: 12 | tableName: custom_user 13 | columns: 14 | - column: 15 | name: id 16 | type: int 17 | autoIncrement: true 18 | startWith: 1 19 | incrementBy: 1 20 | constraints: 21 | primaryKey: true 22 | primaryKeyName: custom_user_pk 23 | - column: 24 | constraints: 25 | nullable: false 26 | name: username 27 | type: VARCHAR(100) 28 | - column: 29 | constraints: 30 | nullable: false 31 | name: email 32 | type: VARCHAR(100) 33 | - column: 34 | constraints: 35 | nullable: false 36 | name: password 37 | type: VARCHAR(100) 38 | - column: 39 | constraints: 40 | nullable: false 41 | name: role 42 | type: VARCHAR(100) 43 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CourseRepositoryCustomImpl.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | 6 | import jakarta.persistence.EntityManager; 7 | import jakarta.persistence.PersistenceContext; 8 | 9 | public class CourseRepositoryCustomImpl implements CourseRepositoryCustom { 10 | 11 | @PersistenceContext 12 | private EntityManager entityManager; 13 | 14 | @Override 15 | public Optional findDTOByUuid(UUID uuid) { 16 | CourseDTO result = (CourseDTO) entityManager 17 | .createQuery( 18 | "select course.uuid as course_uuid, course.name as course_name, course.description as course_description, course.numberOfStudents as course_numberOfStudents, " 19 | + " curriculumItems.serialNumber as curriculumItems_serialNumber, curriculumItems.uuid as curriculumItems_uuid, curriculumItems.title as curriculumItems_title, curriculumItems.description as curriculumItems_description, curriculumItems.class as curriculumItems_type" 20 | + " from com.educational.platform.courses.course.Course course left join course.curriculumItems curriculumItems WHERE course.uuid = :uuid") 21 | .setParameter("uuid", uuid) 22 | .unwrap(org.hibernate.query.Query.class) 23 | .setTupleTransformer(new CourseDTOResultTransformer()) 24 | .getSingleResult(); 25 | 26 | return Optional.ofNullable(result); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CourseFactory.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import com.educational.platform.courses.course.create.CreateCourseCommand; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Component; 6 | 7 | import jakarta.validation.ConstraintViolation; 8 | import jakarta.validation.ConstraintViolationException; 9 | import jakarta.validation.Validator; 10 | import java.util.Set; 11 | 12 | /** 13 | * Represents Course Factory. 14 | */ 15 | @RequiredArgsConstructor 16 | @Component 17 | public class CourseFactory { 18 | 19 | private final Validator validator; 20 | private final CurrentUserAsTeacher currentUserAsTeacher; 21 | 22 | /** 23 | * Creates course from command. 24 | * 25 | * @param courseCommand course command 26 | * @return course 27 | * @throws ConstraintViolationException in the case of validation issues 28 | */ 29 | public Course createFrom(CreateCourseCommand courseCommand) { 30 | final Set> violations = validator.validate(courseCommand); 31 | if (!violations.isEmpty()) { 32 | throw new ConstraintViolationException(violations); 33 | } 34 | 35 | var teacher = currentUserAsTeacher.userAsTeacher(); 36 | return new Course(courseCommand, teacher.getId()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/rating/update/UpdateCourseRatingCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.rating.update; 2 | 3 | import com.educational.platform.common.exception.ResourceNotFoundException; 4 | import com.educational.platform.courses.course.Course; 5 | import com.educational.platform.courses.course.CourseRepository; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import org.axonframework.commandhandling.CommandHandler; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import java.util.Optional; 13 | 14 | /** 15 | * Command handler for {@link UpdateCourseRatingCommand} updates a rating course. 16 | */ 17 | @RequiredArgsConstructor 18 | @Component 19 | @Transactional 20 | public class UpdateCourseRatingCommandHandler { 21 | 22 | private final CourseRepository repository; 23 | 24 | @CommandHandler 25 | public void handle(UpdateCourseRatingCommand command) { 26 | final Optional dbResult = repository.findByUuid(command.getUuid()); 27 | if (dbResult.isEmpty()) { 28 | throw new ResourceNotFoundException(String.format("Course with uuid: %s not found", command.getUuid())); 29 | } 30 | 31 | final Course course = dbResult.get(); 32 | course.updateRating(command.getRating()); 33 | repository.save(course); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /users/application/src/main/java/com/educational/platform/users/PasswordConstraintValidator.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users; 2 | 3 | 4 | import org.passay.*; 5 | import org.springframework.stereotype.Component; 6 | 7 | import jakarta.validation.ConstraintValidator; 8 | import jakarta.validation.ConstraintValidatorContext; 9 | import java.util.Arrays; 10 | 11 | /** 12 | * Represents custom Constraint Validator for validating password. @{@link ValidPassword} annotation type is handled by this implementation. 13 | */ 14 | @Component 15 | public class PasswordConstraintValidator implements ConstraintValidator { 16 | 17 | @Override 18 | public boolean isValid(String password, ConstraintValidatorContext context) { 19 | 20 | // null values are valid 21 | if (password == null) { 22 | return true; 23 | } 24 | 25 | final PasswordValidator validator = new PasswordValidator(Arrays.asList( 26 | new LengthRule(8, 30), 27 | new WhitespaceRule())); 28 | 29 | final RuleResult result = validator.validate(new PasswordData(password)); 30 | if (result.isValid()) { 31 | return true; 32 | } 33 | context.disableDefaultConstraintViolation(); 34 | validator.getMessages(result) 35 | .forEach(message -> context.buildConstraintViolationWithTemplate(message).addConstraintViolation()); 36 | return false; 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/numberofsudents/update/IncreaseNumberOfStudentsCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.numberofsudents.update; 2 | 3 | import com.educational.platform.common.exception.ResourceNotFoundException; 4 | import com.educational.platform.courses.course.Course; 5 | import com.educational.platform.courses.course.CourseRepository; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import org.axonframework.commandhandling.CommandHandler; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import java.util.Optional; 13 | 14 | /** 15 | * Command handler for {@link IncreaseNumberOfStudentsCommand} increases a number of students. 16 | */ 17 | @RequiredArgsConstructor 18 | @Component 19 | @Transactional 20 | public class IncreaseNumberOfStudentsCommandHandler { 21 | 22 | private final CourseRepository repository; 23 | 24 | @CommandHandler 25 | public void handle(IncreaseNumberOfStudentsCommand command) { 26 | final Optional dbResult = repository.findByUuid(command.getUuid()); 27 | if (dbResult.isEmpty()) { 28 | throw new ResourceNotFoundException(String.format("Course with uuid: %s not found", command.getUuid())); 29 | } 30 | 31 | final Course course = dbResult.get(); 32 | course.increaseNumberOfStudents(); 33 | repository.save(course); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CurriculumItem.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.UUID; 4 | 5 | import jakarta.persistence.DiscriminatorColumn; 6 | import jakarta.persistence.DiscriminatorValue; 7 | import jakarta.persistence.Entity; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.GenerationType; 10 | import jakarta.persistence.Id; 11 | import jakarta.persistence.Inheritance; 12 | import jakarta.persistence.InheritanceType; 13 | import jakarta.persistence.JoinColumn; 14 | import jakarta.persistence.ManyToOne; 15 | 16 | /** 17 | * Represents Curriculum Item domain model. 18 | */ 19 | @Entity 20 | @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 21 | @DiscriminatorColumn(name = "type") 22 | @DiscriminatorValue("null") 23 | public abstract class CurriculumItem { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.TABLE) 27 | private Integer id; 28 | 29 | private UUID uuid; 30 | 31 | private String title; 32 | private String description; 33 | private Integer serialNumber; 34 | 35 | @ManyToOne 36 | @JoinColumn(name = "course_id") 37 | private Course course; 38 | 39 | // for JPA 40 | protected CurriculumItem() { 41 | 42 | } 43 | 44 | protected CurriculumItem(String title, String description, Course course, Integer serialNumber) { 45 | this.uuid = UUID.randomUUID(); 46 | this.title = title; 47 | this.description = description; 48 | this.course = course; 49 | this.serialNumber = serialNumber; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /administration/web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation(project(":web")) 4 | implementation(project(":security:security-config")) 5 | implementation(project(":administration:administration-application")) 6 | 7 | implementation("org.springframework.boot", "spring-boot-starter-web") 8 | implementation("org.springframework.boot", "spring-boot-starter-validation") 9 | 10 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 11 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 12 | 13 | testImplementation("org.springframework.boot", "spring-boot-starter-test") 14 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 15 | testImplementation("org.junit.platform", "junit-platform-engine") 16 | testImplementation("org.junit.platform", "junit-platform-launcher") 17 | testImplementation("org.mockito", "mockito-junit-jupiter", libs.versions.mockito.get()) 18 | testImplementation("org.assertj", "assertj-core", libs.versions.assertj.get()) 19 | testImplementation("io.rest-assured", "rest-assured", libs.versions.restAssured.get()) 20 | testImplementation("io.rest-assured", "json-path", libs.versions.restAssured.get()) 21 | testImplementation("io.rest-assured", "xml-path", libs.versions.restAssured.get()) 22 | 23 | testCompileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 24 | testAnnotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 25 | } 26 | 27 | tasks.test { 28 | useJUnitPlatform() 29 | } 30 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/query/ListCourseEnrollmentsQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.query; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import org.axonframework.queryhandling.QueryHandler; 7 | import org.springframework.lang.NonNull; 8 | import org.springframework.security.access.prepost.PreAuthorize; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | import org.springframework.stereotype.Component; 12 | 13 | import com.educational.platform.course.enrollments.CourseEnrollmentDTO; 14 | import com.educational.platform.course.enrollments.CourseEnrollmentRepository; 15 | 16 | import lombok.RequiredArgsConstructor; 17 | 18 | /** 19 | * Query handler for getting the course enrollments. 20 | */ 21 | @RequiredArgsConstructor 22 | @Component 23 | public class ListCourseEnrollmentsQueryHandler { 24 | 25 | private final CourseEnrollmentRepository repository; 26 | 27 | /** 28 | * Retrieves course enrollments. 29 | * 30 | * @param query query. 31 | * @return corresponding course enrollment dtos. 32 | */ 33 | @QueryHandler 34 | @NonNull 35 | @PreAuthorize("hasRole('STUDENT')") 36 | public List handle(ListCourseEnrollmentsQuery query) { 37 | var principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 38 | var username = principal.getUsername(); 39 | 40 | return repository.query(username); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /courses/application/src/test/java/com/educational/platform/courses/teacher/create/CreateTeacherCommandHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.teacher.create; 2 | 3 | import com.educational.platform.courses.teacher.Teacher; 4 | import com.educational.platform.courses.teacher.TeacherRepository; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.ArgumentCaptor; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.mockito.Mockito.verify; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | public class CreateTeacherCommandHandlerTest { 17 | 18 | @Mock 19 | private TeacherRepository repository; 20 | 21 | @InjectMocks 22 | private CreateTeacherCommandHandler sut; 23 | 24 | 25 | @Test 26 | void handle_validTeacher_saveExecuted() { 27 | // given 28 | final CreateTeacherCommand command = new CreateTeacherCommand("username"); 29 | 30 | // when 31 | sut.handle(command); 32 | 33 | // then 34 | final ArgumentCaptor argument = ArgumentCaptor.forClass(Teacher.class); 35 | verify(repository).save(argument.capture()); 36 | final Teacher saved = argument.getValue(); 37 | assertThat(saved) 38 | .hasFieldOrPropertyWithValue("username", "username") 39 | .hasFieldOrProperty("id").isNotNull(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /course-enrollments/application/src/main/java/com/educational/platform/course/enrollments/query/CourseEnrollmentByUUIDQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.query; 2 | 3 | import java.util.Optional; 4 | 5 | import org.axonframework.queryhandling.QueryHandler; 6 | import org.springframework.lang.NonNull; 7 | import org.springframework.security.access.prepost.PreAuthorize; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.security.core.userdetails.UserDetails; 10 | import org.springframework.stereotype.Component; 11 | 12 | import com.educational.platform.course.enrollments.CourseEnrollmentDTO; 13 | import com.educational.platform.course.enrollments.CourseEnrollmentRepository; 14 | 15 | import lombok.RequiredArgsConstructor; 16 | 17 | /** 18 | * Query handler for getting the course enrollment by uuid. 19 | */ 20 | @RequiredArgsConstructor 21 | @Component 22 | public class CourseEnrollmentByUUIDQueryHandler { 23 | 24 | private final CourseEnrollmentRepository repository; 25 | 26 | /** 27 | * Retrieves course enrollment by uuid. 28 | * 29 | * @param query query. 30 | * @return corresponding course enrollment dto. 31 | */ 32 | @QueryHandler 33 | @NonNull 34 | @PreAuthorize("hasRole('STUDENT')") 35 | public Optional handle(CourseEnrollmentByUUIDQuery query) { 36 | var principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 37 | var username = principal.getUsername(); 38 | 39 | return repository.query(query.getUuid(), username); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/approve/ApproveCourseCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.approve; 2 | 3 | import com.educational.platform.common.exception.ResourceNotFoundException; 4 | import com.educational.platform.courses.course.Course; 5 | import com.educational.platform.courses.course.CourseRepository; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import org.axonframework.commandhandling.CommandHandler; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import java.util.Optional; 13 | 14 | /** 15 | * Command handler for {@link ApproveCourseCommand} approves a course. 16 | */ 17 | @RequiredArgsConstructor 18 | @Component 19 | @Transactional 20 | public class ApproveCourseCommandHandler { 21 | 22 | private final CourseRepository repository; 23 | 24 | /** 25 | * Handles approve course command. Approves and save approved course 26 | * 27 | * @param command command 28 | * @throws ResourceNotFoundException if resource not found 29 | */ 30 | @CommandHandler 31 | public void handle(ApproveCourseCommand command) { 32 | final Optional dbResult = repository.findByUuid(command.getUuid()); 33 | if (dbResult.isEmpty()) { 34 | throw new ResourceNotFoundException(String.format("Course with uuid: %s not found", command.getUuid())); 35 | } 36 | 37 | final Course course = dbResult.get(); 38 | course.approve(); 39 | repository.save(course); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /courses/application/src/test/java/com/educational/platform/courses/teacher/create/integration/CreateTeacherCommandHandlerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.teacher.create.integration; 2 | 3 | import com.educational.platform.courses.teacher.Teacher; 4 | import com.educational.platform.courses.teacher.TeacherRepository; 5 | import com.educational.platform.courses.teacher.create.CreateTeacherCommand; 6 | import com.educational.platform.courses.teacher.create.CreateTeacherCommandHandler; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | @SpringBootTest 15 | public class CreateTeacherCommandHandlerIntegrationTest { 16 | 17 | @Autowired 18 | private TeacherRepository repository; 19 | 20 | @MockitoSpyBean 21 | private CreateTeacherCommandHandler sut; 22 | 23 | @Test 24 | void handle_validTeacher_saveExecuted() { 25 | // given 26 | final String username = "username"; 27 | final CreateTeacherCommand command = new CreateTeacherCommand(username); 28 | 29 | // when 30 | sut.handle(command); 31 | 32 | // then 33 | final Teacher saved = repository.findByUsername(username); 34 | assertThat(saved) 35 | .hasFieldOrPropertyWithValue("username", username) 36 | .hasFieldOrProperty("id").isNotNull(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /course-reviews/web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation(project(":web")) 4 | implementation(project(":course-reviews:course-reviews-application")) 5 | 6 | implementation("org.springframework.boot", "spring-boot-starter-web") 7 | implementation("org.springframework.boot", "spring-boot-starter-validation") 8 | 9 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 10 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 11 | 12 | testImplementation(project(":users:users-web")) 13 | testImplementation(project(":security:security-test")) 14 | testImplementation("org.springframework.boot", "spring-boot-starter-test") 15 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 16 | testImplementation("org.junit.platform", "junit-platform-engine") 17 | testImplementation("org.junit.platform", "junit-platform-launcher") 18 | testImplementation("org.mockito", "mockito-junit-jupiter", libs.versions.mockito.get()) 19 | testImplementation("org.assertj", "assertj-core", libs.versions.assertj.get()) 20 | testImplementation("io.rest-assured", "rest-assured", libs.versions.restAssured.get()) 21 | testImplementation("io.rest-assured", "json-path", libs.versions.restAssured.get()) 22 | testImplementation("io.rest-assured", "xml-path", libs.versions.restAssured.get()) 23 | 24 | testCompileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 25 | testAnnotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 26 | } 27 | 28 | tasks.test { 29 | useJUnitPlatform() 30 | } 31 | 32 | -------------------------------------------------------------------------------- /users/web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation(project(":security:security-config")) 4 | implementation(project(":web")) 5 | implementation(project(":users:users-application")) 6 | 7 | implementation("org.springframework.boot", "spring-boot-starter-web") 8 | implementation("org.springframework.boot", "spring-boot-starter-security") 9 | implementation("org.springframework.boot", "spring-boot-starter-validation") 10 | 11 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 12 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 13 | 14 | testImplementation("org.springframework.boot", "spring-boot-starter-test") 15 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 16 | testImplementation("org.junit.platform", "junit-platform-engine") 17 | testImplementation("org.junit.platform", "junit-platform-launcher") 18 | testImplementation("org.mockito", "mockito-junit-jupiter", libs.versions.mockito.get()) 19 | testImplementation("org.assertj", "assertj-core", libs.versions.assertj.get()) 20 | testImplementation("io.rest-assured", "rest-assured", libs.versions.restAssured.get()) 21 | testImplementation("io.rest-assured", "json-path", libs.versions.restAssured.get()) 22 | testImplementation("io.rest-assured", "xml-path", libs.versions.restAssured.get()) 23 | 24 | testCompileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 25 | testAnnotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 26 | } 27 | 28 | tasks.test { 29 | useJUnitPlatform() 30 | } 31 | 32 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/CourseReview.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews; 2 | 3 | import com.educational.platform.common.domain.AggregateRoot; 4 | import com.educational.platform.course.reviews.create.ReviewCourseCommand; 5 | import com.educational.platform.course.reviews.edit.UpdateCourseReviewCommand; 6 | import com.educational.platform.course.reviews.reviewer.Reviewer; 7 | 8 | import jakarta.persistence.*; 9 | import java.util.UUID; 10 | 11 | /** 12 | * Represents Course Review domain model. 13 | */ 14 | @Entity 15 | public class CourseReview implements AggregateRoot { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Integer id; 20 | 21 | private UUID uuid; 22 | 23 | private Integer reviewer; 24 | private Integer course; 25 | private CourseRating rating; 26 | private Comment comment; 27 | 28 | // for JPA 29 | private CourseReview() { 30 | 31 | } 32 | 33 | CourseReview(ReviewCourseCommand command, Integer course, Integer reviewer) { 34 | this.uuid = UUID.randomUUID(); 35 | this.course = course; 36 | this.reviewer = reviewer; 37 | this.rating = new CourseRating(command.getRating()); 38 | this.comment = new Comment(command.getComment()); 39 | } 40 | 41 | public void update(UpdateCourseReviewCommand command) { 42 | this.rating = new CourseRating(command.getRating()); 43 | this.comment = new Comment(command.getComment()); 44 | } 45 | 46 | public UUID toIdentifier() { 47 | return this.uuid; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /course-reviews/application/src/test/java/com/educational/platform/course/reviews/jpa/CourseReviewRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.jpa; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.UUID; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 10 | import org.springframework.test.context.jdbc.Sql; 11 | 12 | import com.educational.platform.course.reviews.CourseReviewRepository; 13 | 14 | @Sql(scripts = "classpath:course_review.sql") 15 | @DataJpaTest 16 | public class CourseReviewRepositoryTest { 17 | 18 | public static final UUID COURSE_UUID = UUID.fromString("123e4567-e89b-12d3-a456-426655440000"); 19 | public static final UUID COURSE_REVIEW_UUID = UUID.fromString("123e4567-e89b-12d3-a456-426655440001"); 20 | public static final String REVIEWER_USERNAME = "reviewer"; 21 | 22 | @Autowired 23 | private CourseReviewRepository sut; 24 | 25 | @Test 26 | void listCourseReviews_unpaged_courseReviews() { 27 | // given/when 28 | var result = sut.listCourseReviews(COURSE_UUID); 29 | 30 | // then 31 | assertThat(result).hasSize(1); 32 | } 33 | 34 | @Test 35 | void isReviewer_validReviewer_true() { 36 | // given/when 37 | var result = sut.isReviewer(COURSE_REVIEW_UUID, REVIEWER_USERNAME); 38 | 39 | // then 40 | assertThat(result).isTrue(); 41 | } 42 | 43 | @Test 44 | void isReviewer_inValidReviewer_false() { 45 | // given/when 46 | var result = sut.isReviewer(COURSE_REVIEW_UUID, "another-reviewer"); 47 | 48 | // then 49 | assertThat(result).isFalse(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/create/CreateCourseCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.create; 2 | 3 | import com.educational.platform.courses.course.Course; 4 | import com.educational.platform.courses.course.CourseFactory; 5 | import com.educational.platform.courses.course.CourseRepository; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import org.axonframework.commandhandling.CommandHandler; 9 | import org.springframework.lang.NonNull; 10 | import org.springframework.security.access.prepost.PreAuthorize; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import jakarta.validation.ConstraintViolationException; 15 | import java.util.UUID; 16 | 17 | /** 18 | * Command handler for {@link CreateCourseCommand} creates a course. 19 | */ 20 | @RequiredArgsConstructor 21 | @Component 22 | @Transactional 23 | public class CreateCourseCommandHandler { 24 | 25 | private final CourseRepository courseRepository; 26 | private final CourseFactory courseFactory; 27 | 28 | /** 29 | * Creates course from command. 30 | * 31 | * @param command command 32 | * @throws ConstraintViolationException in the case of validation issues 33 | */ 34 | // todo move to factory 35 | @CommandHandler 36 | @NonNull 37 | @PreAuthorize("hasRole('TEACHER')") 38 | public UUID handle(CreateCourseCommand command) { 39 | final Course course = courseFactory.createFrom(command); 40 | courseRepository.save(course); 41 | 42 | return course.toIdentity(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /courses/web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation(project(":web")) 4 | implementation(project(":security:security-config")) 5 | implementation(project(":courses:courses-application")) 6 | 7 | implementation("org.springframework.boot", "spring-boot-starter-web") 8 | implementation("org.springframework.boot", "spring-boot-starter-validation") 9 | 10 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 11 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 12 | 13 | testImplementation(project(":users:users-web")) 14 | testImplementation(project(":security:security-test")) 15 | testImplementation("org.springframework.boot", "spring-boot-starter-test") 16 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 17 | testImplementation("org.junit.platform", "junit-platform-engine") 18 | testImplementation("org.junit.platform", "junit-platform-launcher") 19 | testImplementation("org.mockito", "mockito-junit-jupiter", libs.versions.mockito.get()) 20 | testImplementation("org.assertj", "assertj-core", libs.versions.assertj.get()) 21 | testImplementation("io.rest-assured", "rest-assured", libs.versions.restAssured.get()) 22 | testImplementation("io.rest-assured", "json-path", libs.versions.restAssured.get()) 23 | testImplementation("io.rest-assured", "xml-path", libs.versions.restAssured.get()) 24 | 25 | testCompileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 26 | testAnnotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 27 | } 28 | 29 | tasks.test { 30 | useJUnitPlatform() 31 | } 32 | 33 | -------------------------------------------------------------------------------- /users/application/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation(project(":security:security-config")) 4 | implementation(project(":users:users-integration-events")) 5 | 6 | implementation("org.springframework.boot", "spring-boot-starter-web") 7 | implementation("org.springframework.boot", "spring-boot-starter-validation") 8 | implementation("org.springframework.boot", "spring-boot-starter-security") 9 | implementation("org.springframework.boot", "spring-boot-starter-data-jpa") 10 | implementation("io.jsonwebtoken", "jjwt", libs.versions.jsonwebtoken.get()) 11 | implementation("javax.xml.bind", "jaxb-api", libs.versions.jaxbApi.get()) 12 | implementation("org.passay", "passay", libs.versions.passay.get()) 13 | 14 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 15 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 16 | 17 | testImplementation("org.springframework.boot", "spring-boot-starter-test") 18 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 19 | testImplementation("org.junit.platform", "junit-platform-engine") 20 | testImplementation("org.junit.platform", "junit-platform-launcher") 21 | testImplementation("io.rest-assured", "rest-assured", libs.versions.restAssured.get()) 22 | testImplementation("io.rest-assured", "json-path", libs.versions.restAssured.get()) 23 | testImplementation("io.rest-assured", "xml-path", libs.versions.restAssured.get()) 24 | } 25 | 26 | tasks.compileJava { 27 | options.compilerArgs.add("-Amapstruct.defaultComponentModel=spring") 28 | } 29 | 30 | tasks.test { 31 | useJUnitPlatform() 32 | } 33 | -------------------------------------------------------------------------------- /course-enrollments/application/src/test/java/com/educational/platform/course/enrollments/query/security/CourseEnrollmentByUUIDQueryHandlerSecurityTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.query.security; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 4 | 5 | import java.util.UUID; 6 | 7 | import org.assertj.core.api.ThrowableAssert.ThrowingCallable; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; 11 | import org.springframework.security.access.AccessDeniedException; 12 | import org.springframework.security.test.context.support.WithMockUser; 13 | import org.springframework.test.context.jdbc.Sql; 14 | 15 | import com.educational.platform.course.enrollments.query.CourseEnrollmentByUUIDQuery; 16 | import com.educational.platform.course.enrollments.query.CourseEnrollmentByUUIDQueryHandler; 17 | 18 | @Sql(scripts = "classpath:course.sql") 19 | @SpringBootTest(properties = "com.educational.platform.security.enabled=true") 20 | public class CourseEnrollmentByUUIDQueryHandlerSecurityTest { 21 | 22 | @MockitoSpyBean 23 | private CourseEnrollmentByUUIDQueryHandler sut; 24 | 25 | @Test 26 | @WithMockUser(roles = "TEACHER") 27 | void handle_userIsTeacher_accessDeniedException() { 28 | // given 29 | var command = new CourseEnrollmentByUUIDQuery(UUID.fromString("123e4567-e89b-12d3-a456-426655440001")); 30 | 31 | // when 32 | final ThrowingCallable queryAction = () -> sut.handle(command); 33 | 34 | // then 35 | assertThatThrownBy(queryAction).isInstanceOf(AccessDeniedException.class); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /course-enrollments/application/src/test/java/com/educational/platform/course/enrollments/query/security/ListCourseEnrollmentsQueryHandlerSecurityTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.query.security; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 4 | 5 | import java.util.UUID; 6 | 7 | import org.assertj.core.api.ThrowableAssert.ThrowingCallable; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; 11 | import org.springframework.security.access.AccessDeniedException; 12 | import org.springframework.security.test.context.support.WithMockUser; 13 | import org.springframework.test.context.jdbc.Sql; 14 | 15 | import com.educational.platform.course.enrollments.query.CourseEnrollmentByUUIDQuery; 16 | import com.educational.platform.course.enrollments.query.CourseEnrollmentByUUIDQueryHandler; 17 | 18 | @Sql(scripts = "classpath:course.sql") 19 | @SpringBootTest(properties = "com.educational.platform.security.enabled=true") 20 | public class ListCourseEnrollmentsQueryHandlerSecurityTest { 21 | 22 | @MockitoSpyBean 23 | private CourseEnrollmentByUUIDQueryHandler sut; 24 | 25 | @Test 26 | @WithMockUser(roles = "TEACHER") 27 | void handle_userIsTeacher_accessDeniedException() { 28 | // given 29 | var command = new CourseEnrollmentByUUIDQuery(UUID.fromString("123e4567-e89b-12d3-a456-426655440001")); 30 | 31 | // when 32 | final ThrowingCallable queryAction = () -> sut.handle(command); 33 | 34 | // then 35 | assertThatThrownBy(queryAction).isInstanceOf(AccessDeniedException.class); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /course-enrollments/application/src/test/java/com/educational/platform/course/enrollments/student/create/CreateStudentCommandHandlerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.student.create; 2 | 3 | import com.educational.platform.course.enrollments.student.Student; 4 | import com.educational.platform.course.enrollments.student.StudentRepository; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; 10 | import org.springframework.data.domain.Example; 11 | 12 | import java.util.Optional; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @AutoConfigureTestDatabase 17 | @SpringBootTest 18 | public class CreateStudentCommandHandlerIntegrationTest { 19 | 20 | @Autowired 21 | private StudentRepository repository; 22 | 23 | @MockitoSpyBean 24 | private CreateStudentCommandHandler sut; 25 | 26 | @Test 27 | void handle_validCommand_studentSaved() { 28 | // given 29 | final CreateStudentCommand command = new CreateStudentCommand("username"); 30 | 31 | // when 32 | sut.handle(command); 33 | 34 | // then 35 | final Optional saved = repository.findOne(Example.of(new Student(command))); 36 | assertThat(saved).isNotEmpty(); 37 | final Student student = saved.get(); 38 | assertThat(student).hasFieldOrPropertyWithValue("username", "username"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CourseDTOResultTransformer.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.ArrayList; 4 | import java.util.LinkedHashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.UUID; 8 | 9 | import org.hibernate.query.TupleTransformer; 10 | 11 | /** 12 | * Represents health indicator config dto result transformer. 13 | */ 14 | public class CourseDTOResultTransformer implements TupleTransformer { 15 | 16 | private final Map courseDTOMap = new LinkedHashMap<>(); 17 | 18 | @Override 19 | public CourseDTO transformTuple( 20 | Object[] tuple, String[] aliases 21 | ) { 22 | 23 | Map aliasToIndexMap = aliasToIndexMap(aliases); 24 | 25 | UUID uuid = UUID.fromString(String.valueOf(tuple[aliasToIndexMap.get(CourseDTO.UUID_COLUMN)])); 26 | 27 | CourseDTO courseDTO = courseDTOMap.computeIfAbsent( 28 | uuid, 29 | id -> new CourseDTO(tuple, aliasToIndexMap) 30 | ); 31 | if (aliasToIndexMap.get(CurriculumItemDTO.TYPE).toString().equals("Lecture")) { 32 | courseDTO.getCurriculumItems().add(new LectureDTO(uuid, tuple, aliasToIndexMap)); 33 | } else if (aliasToIndexMap.get(CurriculumItemDTO.TYPE).toString().equals("Quiz")) { 34 | courseDTO.getCurriculumItems().add(new QuizDTO(uuid, tuple, aliasToIndexMap)); 35 | } 36 | return courseDTO; 37 | } 38 | 39 | public Map aliasToIndexMap( 40 | String[] aliases 41 | ) { 42 | 43 | Map aliasToIndexMap = new LinkedHashMap<>(); 44 | 45 | for (int i = 0; i < aliases.length; i++) { 46 | aliasToIndexMap.put(aliases[i], i); 47 | } 48 | 49 | return aliasToIndexMap; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /users/web/src/main/java/com/educational/platform/users/security/UserController.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users.security; 2 | 3 | import jakarta.validation.Valid; 4 | 5 | import com.educational.platform.users.login.SignInCommand; 6 | 7 | import org.axonframework.commandhandling.gateway.CommandGateway; 8 | import org.springframework.validation.annotation.Validated; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import com.educational.platform.users.registration.UserRegistrationCommand; 15 | 16 | import lombok.RequiredArgsConstructor; 17 | 18 | @RequiredArgsConstructor 19 | @RestController 20 | @RequestMapping("/users") 21 | @Validated 22 | public class UserController { 23 | 24 | private final CommandGateway commandGateway; 25 | 26 | @PostMapping("/sign-up") 27 | public String signUp(@Valid @RequestBody SignUpRequest signUpRequest) { 28 | UserRegistrationCommand command = UserRegistrationCommand 29 | .builder() 30 | .role(signUpRequest.getRole()) 31 | .email(signUpRequest.getEmail()) 32 | .username(signUpRequest.getUsername()) 33 | .password(signUpRequest.getPassword()) 34 | .build(); 35 | 36 | return commandGateway.sendAndWait(command); 37 | } 38 | 39 | @PostMapping("/sign-in") 40 | public String signIn(@Valid @RequestBody SignInRequest signInRequest) { 41 | SignInCommand command = SignInCommand 42 | .builder() 43 | .username(signInRequest.getUsername()) 44 | .password(signInRequest.getPassword()) 45 | .build(); 46 | 47 | return commandGateway.sendAndWait(command); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /courses/application/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation(project(":security:security-config")) 4 | implementation(project(":courses:courses-integration-events")) 5 | implementation(project(":administration:administration-integration-events")) 6 | implementation(project(":course-enrollments:course-enrollments-integration-events")) 7 | implementation(project(":course-reviews:course-reviews-integration-events")) 8 | implementation(project(":users:users-integration-events")) 9 | 10 | implementation("org.springframework.boot", "spring-boot-starter-security") 11 | implementation("org.springframework.boot", "spring-boot-starter-data-jpa") 12 | implementation("org.springframework.boot", "spring-boot-starter-validation") 13 | 14 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 15 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 16 | 17 | testImplementation("org.springframework.boot", "spring-boot-starter-test") 18 | testImplementation("org.springframework.security", "spring-security-test") 19 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 20 | testImplementation("org.junit.platform", "junit-platform-engine") 21 | testImplementation("org.junit.platform", "junit-platform-launcher") 22 | testImplementation("org.mockito", "mockito-junit-jupiter", libs.versions.mockito.get()) 23 | testImplementation("org.assertj", "assertj-core", libs.versions.assertj.get()) 24 | 25 | testCompileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 26 | testAnnotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 27 | } 28 | 29 | tasks.test { 30 | useJUnitPlatform() 31 | } 32 | 33 | -------------------------------------------------------------------------------- /configuration/src/test/java/com/educational/platform/handler/CommandHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.handler; 2 | 3 | import com.tngtech.archunit.base.DescribedPredicate; 4 | import com.tngtech.archunit.core.domain.JavaClass; 5 | import com.tngtech.archunit.junit.AnalyzeClasses; 6 | import com.tngtech.archunit.junit.ArchTest; 7 | import com.tngtech.archunit.lang.ArchRule; 8 | 9 | import org.axonframework.commandhandling.CommandHandler; 10 | import org.springframework.lang.NonNull; 11 | import org.springframework.lang.Nullable; 12 | 13 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; 14 | 15 | @AnalyzeClasses(packages = "com.educational.platform") 16 | public class CommandHandlerTest { 17 | 18 | @ArchTest 19 | public static final ArchRule commandHandlers_shouldHave_nullabilityAnnotations = methods() 20 | .that().arePublic() 21 | .and().doNotHaveRawReturnType(new DescribedPredicate<>("native or void") { 22 | @Override 23 | public boolean test(JavaClass javaClass) { 24 | return javaClass.isPrimitive() || javaClass.isEquivalentTo(Void.TYPE); 25 | } 26 | }) 27 | .and().areAnnotatedWith(CommandHandler.class) 28 | .should().beAnnotatedWith(NonNull.class).orShould().beAnnotatedWith(Nullable.class) 29 | .because("Command handlers should provide detailed documentation of API, it's why nullability annotations are needed."); 30 | 31 | @ArchTest 32 | public static final ArchRule commands_shouldBe_immutable = fields() 33 | .that().areDeclaredInClassesThat().haveSimpleNameEndingWith("Command") 34 | .should().beFinal() 35 | .because("Commands should be immutable."); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /course-enrollments/web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":common")) 3 | implementation(project(":web")) 4 | implementation(project(":security:security-config")) 5 | implementation(project(":course-enrollments:course-enrollments-application")) 6 | 7 | implementation("org.springframework.boot", "spring-boot-starter-web") 8 | implementation("org.springframework.boot", "spring-boot-starter-validation") 9 | 10 | compileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 11 | annotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 12 | 13 | testImplementation(project(":users:users-web")) 14 | testImplementation(project(":security:security-test")) 15 | testImplementation("org.springframework.boot", "spring-boot-starter-test") 16 | testImplementation("org.springframework.security", "spring-security-test") 17 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 18 | testImplementation("org.junit.platform", "junit-platform-engine") 19 | testImplementation("org.junit.platform", "junit-platform-launcher") 20 | testImplementation("org.mockito", "mockito-junit-jupiter", libs.versions.mockito.get()) 21 | testImplementation("org.assertj", "assertj-core", libs.versions.assertj.get()) 22 | testImplementation("io.rest-assured", "rest-assured", libs.versions.restAssured.get()) 23 | testImplementation("io.rest-assured", "json-path", libs.versions.restAssured.get()) 24 | testImplementation("io.rest-assured", "xml-path", libs.versions.restAssured.get()) 25 | 26 | testCompileOnly("org.projectlombok", "lombok", libs.versions.lombok.get()) 27 | testAnnotationProcessor("org.projectlombok", "lombok", libs.versions.lombok.get()) 28 | } 29 | 30 | tasks.test { 31 | useJUnitPlatform() 32 | } 33 | -------------------------------------------------------------------------------- /course-reviews/application/src/test/java/com/educational/platform/course/reviews/reviewer/create/integration/CreateReviewerCommandHandlerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.reviewer.create.integration; 2 | 3 | import com.educational.platform.course.reviews.reviewer.Reviewer; 4 | import com.educational.platform.course.reviews.reviewer.ReviewerRepository; 5 | import com.educational.platform.course.reviews.reviewer.create.CreateReviewerCommand; 6 | import com.educational.platform.course.reviews.reviewer.create.CreateReviewerCommandHandler; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; 11 | import org.springframework.data.domain.Example; 12 | 13 | import java.util.Optional; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | @SpringBootTest 18 | public class CreateReviewerCommandHandlerIntegrationTest { 19 | 20 | @Autowired 21 | private ReviewerRepository repository; 22 | 23 | @MockitoSpyBean 24 | private CreateReviewerCommandHandler sut; 25 | 26 | @Test 27 | void handle_validCommand_reviewerSaved() { 28 | // given 29 | final CreateReviewerCommand command = new CreateReviewerCommand("username"); 30 | 31 | // when 32 | sut.handle(command); 33 | 34 | // then 35 | final Optional saved = repository.findOne(Example.of(new Reviewer(command))); 36 | assertThat(saved).isNotEmpty(); 37 | final Reviewer reviewer = saved.get(); 38 | assertThat(reviewer) 39 | .hasFieldOrPropertyWithValue("username", "username"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /courses/application/src/test/java/com/educational/platform/courses/course/jpa/CourseRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.jpa; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 8 | 9 | import com.educational.platform.courses.course.Course; 10 | import com.educational.platform.courses.course.CourseRepository; 11 | import com.educational.platform.courses.course.create.CreateCourseCommand; 12 | import com.educational.platform.courses.teacher.Teacher; 13 | import com.educational.platform.courses.teacher.TeacherRepository; 14 | import com.educational.platform.courses.teacher.create.CreateTeacherCommand; 15 | 16 | @DataJpaTest 17 | public class CourseRepositoryTest { 18 | 19 | private static final String TEACHER = "teacher"; 20 | 21 | @Autowired 22 | private CourseRepository courseRepository; 23 | 24 | @Autowired 25 | private TeacherRepository teacherRepository; 26 | 27 | @Test 28 | void queryDtoByUUID_validQuery_dtoRetrieved() { 29 | // given 30 | var createTeacherCommand = new CreateTeacherCommand(TEACHER); 31 | var teacher = new Teacher(createTeacherCommand); 32 | teacherRepository.save(teacher); 33 | var createCourseCommand = CreateCourseCommand.builder().name("name").description("description").build(); 34 | var course = new Course(createCourseCommand, teacher.getId()); 35 | courseRepository.save(course); 36 | 37 | // when 38 | var result = courseRepository.findDTOByUuid(course.toIdentity()); 39 | 40 | // then 41 | assertThat(result).isNotEmpty(); 42 | assertThat(result.get()).hasFieldOrPropertyWithValue("name", "name").hasFieldOrPropertyWithValue("description", "description"); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /courses/application/src/main/java/com/educational/platform/courses/course/CourseRepository.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.UUID; 6 | 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | 11 | /** 12 | * Represents course repository. 13 | */ 14 | public interface CourseRepository extends JpaRepository, CourseRepositoryCustom { 15 | 16 | /** 17 | * Retrieves a course by its uuid. 18 | * 19 | * @param uuid must not be {@literal null}. 20 | * @return the course with the given uuid or {@literal Optional#empty()} if none found. 21 | * @throws IllegalArgumentException if {@literal uuid} is {@literal null}. 22 | */ 23 | Optional findByUuid(UUID uuid); 24 | 25 | /** 26 | * Retrieves a list of course dtos. 27 | * 28 | * @return the list of course dtos. 29 | * @throws IllegalArgumentException if {@literal uuid} is {@literal null}. 30 | */ 31 | @Query(value = "SELECT new com.educational.platform.courses.course.CourseLightDTO(c.uuid, c.name, c.description, c.numberOfStudents) " 32 | + "FROM com.educational.platform.courses.course.Course c") 33 | List list(); 34 | 35 | /** 36 | * Checks if passed username is an username of teacher of course. 37 | * 38 | * @param uuid course uuid. 39 | * @param username username. 40 | * @return true if teacher, false if not. 41 | */ 42 | @Query("select count(c) > 0 from Course c join com.educational.platform.courses.teacher.Teacher r on c.teacher = r.id where c.uuid = :uuid and r.username = :username") 43 | boolean isTeacher(@Param("uuid") UUID uuid, @Param("username") String username); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /course-reviews/application/src/main/java/com/educational/platform/course/reviews/create/ReviewCourseCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.create; 2 | 3 | import com.educational.platform.common.exception.RelatedResourceIsNotResolvedException; 4 | import com.educational.platform.course.reviews.CourseReview; 5 | import com.educational.platform.course.reviews.CourseReviewFactory; 6 | import com.educational.platform.course.reviews.CourseReviewRepository; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | import org.axonframework.commandhandling.CommandHandler; 10 | import org.springframework.lang.NonNull; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import jakarta.validation.ConstraintViolationException; 15 | import java.util.UUID; 16 | 17 | /** 18 | * Command handler for {@link ReviewCourseCommand} creates a course review. 19 | */ 20 | @RequiredArgsConstructor 21 | @Component 22 | @Transactional 23 | public class ReviewCourseCommandHandler { 24 | 25 | private final CourseReviewRepository courseReviewRepository; 26 | private final CourseReviewFactory courseReviewFactory; 27 | 28 | /** 29 | * Creates course review from command. 30 | * 31 | * @param command command 32 | * @return uuid 33 | * @throws ConstraintViolationException in the case of validation issues 34 | * @throws RelatedResourceIsNotResolvedException if course or reviewer is not found by relation 35 | */ 36 | @CommandHandler 37 | @NonNull 38 | public UUID handle(ReviewCourseCommand command) { 39 | final CourseReview courseReview = courseReviewFactory.createFrom(command); 40 | courseReviewRepository.save(courseReview); 41 | 42 | return courseReview.toIdentifier(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /courses/application/src/test/java/com/educational/platform/courses/course/approve/CourseApprovedByAdminIntegrationEventHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.approve; 2 | 3 | import com.educational.platform.administration.integration.event.CourseApprovedByAdminIntegrationEvent; 4 | 5 | import org.axonframework.commandhandling.gateway.CommandGateway; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.ArgumentCaptor; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.util.UUID; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.Mockito.verify; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | public class CourseApprovedByAdminIntegrationEventHandlerTest { 20 | 21 | @Mock 22 | private CommandGateway commandGateway; 23 | 24 | @InjectMocks 25 | private CourseApprovedByAdminIntegrationEventHandler sut; 26 | 27 | @Test 28 | void handleCourseApprovedByAdminEvent_approveCourseCommandExecuted() { 29 | // given 30 | final UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426655440001"); 31 | final CourseApprovedByAdminIntegrationEvent event = new CourseApprovedByAdminIntegrationEvent(uuid); 32 | 33 | // when 34 | sut.handleCourseApprovedByAdminEvent(event); 35 | 36 | // then 37 | final ArgumentCaptor argument = ArgumentCaptor.forClass(ApproveCourseCommand.class); 38 | verify(commandGateway).send(argument.capture()); 39 | final ApproveCourseCommand approveCourseCommand = argument.getValue(); 40 | assertThat(approveCourseCommand) 41 | .hasFieldOrPropertyWithValue("uuid", uuid); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /configuration/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":users:users-application")) 3 | implementation(project(":users:users-web")) 4 | implementation(project(":users:users-integration-events")) 5 | 6 | implementation(project(":administration:administration-application")) 7 | implementation(project(":administration:administration-web")) 8 | implementation(project(":administration:administration-integration-events")) 9 | 10 | implementation(project(":course-enrollments:course-enrollments-application")) 11 | implementation(project(":course-enrollments:course-enrollments-web")) 12 | implementation(project(":course-enrollments:course-enrollments-integration-events")) 13 | 14 | implementation(project(":course-reviews:course-reviews-application")) 15 | implementation(project(":course-reviews:course-reviews-web")) 16 | implementation(project(":course-reviews:course-reviews-integration-events")) 17 | 18 | implementation(project(":courses:courses-application")) 19 | implementation(project(":courses:courses-web")) 20 | implementation(project(":courses:courses-integration-events")) 21 | 22 | implementation(project(":security:security-config")) 23 | implementation(project(":web")) 24 | implementation(project(":common")) 25 | 26 | implementation("org.springframework.boot", "spring-boot-starter-web") 27 | implementation("org.liquibase", "liquibase-core") 28 | 29 | testImplementation("org.junit.jupiter", "junit-jupiter-api") 30 | testImplementation("org.junit.platform", "junit-platform-engine") 31 | testImplementation("org.junit.platform", "junit-platform-launcher") 32 | testImplementation("org.mockito", "mockito-junit-jupiter", libs.versions.mockito.get()) 33 | testImplementation("com.tngtech.archunit", "archunit-junit5", libs.versions.archunit.get()) 34 | } 35 | 36 | tasks.test { 37 | useJUnitPlatform() 38 | } 39 | -------------------------------------------------------------------------------- /users/web/src/main/java/com/educational/platform/users/security/JwtTokenFilter.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.users.security; 2 | 3 | import com.educational.platform.security.JwtTokenValidationException; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.web.filter.OncePerRequestFilter; 9 | 10 | import jakarta.servlet.FilterChain; 11 | import jakarta.servlet.ServletException; 12 | import jakarta.servlet.http.HttpServletRequest; 13 | import jakarta.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | 16 | /** 17 | * Represents jwt token filter, once per request. 18 | */ 19 | @RequiredArgsConstructor 20 | public class JwtTokenFilter extends OncePerRequestFilter { 21 | 22 | private final JwtTokenProvider jwtTokenProvider; 23 | 24 | @Override 25 | protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { 26 | String token = jwtTokenProvider.resolveToken(httpServletRequest); 27 | try { 28 | if (token != null && jwtTokenProvider.validateToken(token)) { 29 | Authentication auth = jwtTokenProvider.getAuthentication(token); 30 | SecurityContextHolder.getContext().setAuthentication(auth); 31 | } 32 | } catch (JwtTokenValidationException ex) { 33 | SecurityContextHolder.clearContext(); 34 | // todo recheck http status code value 35 | httpServletResponse.sendError(HttpStatus.BAD_REQUEST.value(), ex.getMessage()); 36 | return; 37 | } 38 | 39 | filterChain.doFilter(httpServletRequest, httpServletResponse); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /administration/application/src/test/java/com/educational/platform/administration/course/create/SendCourseToApproveIntegrationEventHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.create; 2 | 3 | import com.educational.platform.courses.integration.event.SendCourseToApproveIntegrationEvent; 4 | 5 | import org.axonframework.commandhandling.gateway.CommandGateway; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.ArgumentCaptor; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.util.UUID; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.Mockito.verify; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | class SendCourseToApproveIntegrationEventHandlerTest { 20 | 21 | @Mock 22 | private CommandGateway commandGateway; 23 | 24 | @InjectMocks 25 | private SendCourseToApproveIntegrationEventHandler sut; 26 | 27 | @Test 28 | void handleCourseApprovedByAdminEvent_approveCourseCommandExecuted() { 29 | // given 30 | final UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426655440001"); 31 | final SendCourseToApproveIntegrationEvent event = new SendCourseToApproveIntegrationEvent(uuid); 32 | 33 | // when 34 | sut.handleSendCourseToApproveEvent(event); 35 | 36 | // then 37 | final ArgumentCaptor argument = ArgumentCaptor.forClass(CreateCourseProposalCommand.class); 38 | verify(commandGateway).send(argument.capture()); 39 | final CreateCourseProposalCommand createCourseProposalCommand = argument.getValue(); 40 | assertThat(createCourseProposalCommand) 41 | .hasFieldOrPropertyWithValue("uuid", uuid); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /administration/application/src/test/java/com/educational/platform/administration/course/create/CreateCourseProposalCommandHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.create; 2 | 3 | import com.educational.platform.administration.course.CourseProposal; 4 | import com.educational.platform.administration.course.CourseProposalRepository; 5 | import com.educational.platform.administration.course.create.CreateCourseProposalCommand; 6 | import com.educational.platform.administration.course.create.CreateCourseProposalCommandHandler; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.ArgumentCaptor; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | 14 | import java.util.UUID; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.mockito.Mockito.verify; 18 | 19 | @ExtendWith(MockitoExtension.class) 20 | public class CreateCourseProposalCommandHandlerTest { 21 | 22 | @Mock 23 | private CourseProposalRepository repository; 24 | 25 | @InjectMocks 26 | private CreateCourseProposalCommandHandler sut; 27 | 28 | @Test 29 | void handle_courseProposalSaved() { 30 | // given 31 | final UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426655440001"); 32 | final CreateCourseProposalCommand command = new CreateCourseProposalCommand(uuid); 33 | 34 | // when 35 | sut.handle(command); 36 | 37 | // then 38 | final ArgumentCaptor argument = ArgumentCaptor.forClass(CourseProposal.class); 39 | verify(repository).save(argument.capture()); 40 | final CourseProposal proposal = argument.getValue(); 41 | assertThat(proposal) 42 | .hasFieldOrPropertyWithValue("uuid", uuid); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /course-enrollments/web/src/main/java/com/educational/platform/course/enrollments/CourseEnrollmentController.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments; 2 | 3 | import com.educational.platform.course.enrollments.query.ListCourseEnrollmentsQuery; 4 | import com.educational.platform.course.enrollments.register.RegisterStudentToCourseCommand; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import org.axonframework.commandhandling.gateway.CommandGateway; 8 | import org.axonframework.messaging.responsetypes.ResponseTypes; 9 | import org.axonframework.queryhandling.QueryGateway; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.List; 14 | import java.util.UUID; 15 | 16 | import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; 17 | 18 | /** 19 | * Represents Course Enrollment Controller. 20 | */ 21 | @RestController 22 | @RequiredArgsConstructor 23 | public class CourseEnrollmentController { 24 | 25 | private final CommandGateway commandGateway; 26 | private final QueryGateway queryGateway; 27 | 28 | @PostMapping(value = "/courses/{uuid}/course-enrollments", produces = APPLICATION_JSON_VALUE) 29 | @ResponseStatus(HttpStatus.CREATED) 30 | public UUID enroll(@PathVariable("uuid") UUID uuid, @RequestBody CourseEnrollmentRequest request) { 31 | var command = RegisterStudentToCourseCommand.builder() 32 | .courseId(uuid) 33 | .build(); 34 | 35 | return commandGateway.sendAndWait(command); 36 | } 37 | 38 | @GetMapping(value = "/course-enrollments", produces = APPLICATION_JSON_VALUE) 39 | @ResponseStatus(HttpStatus.OK) 40 | public List courseEnrollments() { 41 | return queryGateway.query(new ListCourseEnrollmentsQuery(), ResponseTypes.multipleInstancesOf(CourseEnrollmentDTO.class)).join(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /course-reviews/application/src/test/java/com/educational/platform/course/reviews/course/create/integration/CreateReviewableCourseCommandHandlerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.reviews.course.create.integration; 2 | 3 | import com.educational.platform.course.reviews.course.ReviewableCourse; 4 | import com.educational.platform.course.reviews.course.ReviewableCourseRepository; 5 | import com.educational.platform.course.reviews.course.create.CreateReviewableCourseCommand; 6 | import com.educational.platform.course.reviews.course.create.CreateReviewableCourseCommandHandler; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; 11 | 12 | import java.util.Optional; 13 | import java.util.UUID; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | @SpringBootTest 18 | public class CreateReviewableCourseCommandHandlerIntegrationTest { 19 | 20 | @Autowired 21 | private ReviewableCourseRepository repository; 22 | 23 | @MockitoSpyBean 24 | private CreateReviewableCourseCommandHandler sut; 25 | 26 | @Test 27 | void handle_validCommand_reviewableCourseSaved() { 28 | // given 29 | final UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426655440001"); 30 | final CreateReviewableCourseCommand command = new CreateReviewableCourseCommand(uuid); 31 | 32 | // when 33 | sut.handle(command); 34 | 35 | // then 36 | final Optional saved = repository.findByOriginalCourseId(uuid); 37 | assertThat(saved).isNotEmpty(); 38 | final ReviewableCourse reviewableCourse = saved.get(); 39 | assertThat(reviewableCourse).hasFieldOrPropertyWithValue("originalCourseId", uuid); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /course-enrollments/web/src/test/java/com/educational/platform/course/enrollments/api/CourseEnrollmentApiTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.course.enrollments.api; 2 | 3 | import com.educational.platform.security.SignUpHelper; 4 | import io.restassured.RestAssured; 5 | import io.restassured.http.ContentType; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.server.LocalServerPort; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.test.context.TestPropertySource; 12 | import org.springframework.test.context.jdbc.Sql; 13 | 14 | import java.util.UUID; 15 | 16 | import static io.restassured.RestAssured.given; 17 | 18 | /** 19 | * Represents API tests for course enrollments functionality. 20 | */ 21 | @Sql(scripts = "classpath:insert_data.sql") 22 | @TestPropertySource("classpath:application-security.properties") 23 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 24 | public class CourseEnrollmentApiTest { 25 | 26 | @LocalServerPort 27 | private int port; 28 | 29 | @BeforeEach 30 | void setup() { 31 | RestAssured.port = port; 32 | } 33 | 34 | @Test 35 | void register_validCourse_createdWithUUID() { 36 | var token = SignUpHelper.signUpStudent(); 37 | 38 | given() 39 | .contentType(ContentType.JSON) 40 | .header("Authorization", "Bearer " + token) 41 | .body("{\n" + 42 | " \"student\": \"username\"\n" + 43 | "}") 44 | 45 | .when() 46 | .post("/courses/{uuid}/course-enrollments", UUID.fromString("123e4567-e89b-12d3-a456-426655440001")) 47 | 48 | .then() 49 | .statusCode(HttpStatus.CREATED.value()); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /administration/application/src/test/java/com/educational/platform/administration/course/create/integration/CreateCourseProposalCommandHandlerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course.create.integration; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Optional; 6 | import java.util.UUID; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.data.domain.Example; 12 | import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; 13 | 14 | import com.educational.platform.administration.course.CourseProposal; 15 | import com.educational.platform.administration.course.CourseProposalRepository; 16 | import com.educational.platform.administration.course.create.CreateCourseProposalCommand; 17 | import com.educational.platform.administration.course.create.CreateCourseProposalCommandHandler; 18 | 19 | @SpringBootTest 20 | public class CreateCourseProposalCommandHandlerIntegrationTest { 21 | 22 | @Autowired 23 | private CourseProposalRepository repository; 24 | 25 | @MockitoSpyBean 26 | private CreateCourseProposalCommandHandler sut; 27 | 28 | @Test 29 | void handle_validCommand_courseProposalSavedWithStatusWaitingForApproval() { 30 | // given 31 | final UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426655440001"); 32 | final CreateCourseProposalCommand command = new CreateCourseProposalCommand(uuid); 33 | 34 | // when 35 | sut.handle(command); 36 | 37 | // then 38 | final Optional saved = repository.findOne(Example.of(new CourseProposal(command))); 39 | assertThat(saved).isNotEmpty(); 40 | final CourseProposal proposal = saved.get(); 41 | assertThat(proposal).hasFieldOrPropertyWithValue("uuid", uuid); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /administration/application/src/main/java/com/educational/platform/administration/course/CourseProposal.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.administration.course; 2 | 3 | import com.educational.platform.administration.course.create.CreateCourseProposalCommand; 4 | import com.educational.platform.common.domain.AggregateRoot; 5 | 6 | import java.util.UUID; 7 | 8 | import jakarta.persistence.Entity; 9 | import jakarta.persistence.EnumType; 10 | import jakarta.persistence.Enumerated; 11 | import jakarta.persistence.GeneratedValue; 12 | import jakarta.persistence.GenerationType; 13 | import jakarta.persistence.Id; 14 | 15 | /** 16 | * Represents Course Proposal domain model. 17 | */ 18 | @Entity 19 | public class CourseProposal implements AggregateRoot { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | private Integer id; 24 | 25 | private UUID uuid; 26 | 27 | @Enumerated(EnumType.STRING) 28 | private CourseProposalStatus status; 29 | 30 | // for JPA 31 | private CourseProposal() { 32 | 33 | } 34 | 35 | public CourseProposal(CreateCourseProposalCommand command) { 36 | this.uuid = command.uuid(); 37 | this.status = CourseProposalStatus.WAITING_FOR_APPROVAL; 38 | } 39 | 40 | public void approve() { 41 | if (status == CourseProposalStatus.APPROVED) { 42 | throw new CourseProposalAlreadyApprovedException(uuid); 43 | } 44 | this.status = CourseProposalStatus.APPROVED; 45 | } 46 | 47 | public void decline() { 48 | if (status == CourseProposalStatus.DECLINED) { 49 | throw new CourseProposalAlreadyDeclinedException(uuid); 50 | } 51 | this.status = CourseProposalStatus.DECLINED; 52 | } 53 | 54 | public CourseProposalDTO toDTO() { 55 | return CourseProposalDTO.builder() 56 | .uuid(uuid) 57 | .status(status.toDTO()) 58 | .build(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /courses/application/src/test/java/com/educational/platform/courses/course/rating/update/CourseRatingRecalculatedIntegrationEventHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.educational.platform.courses.course.rating.update; 2 | 3 | import com.educational.platform.course.reviews.integration.event.CourseRatingRecalculatedIntegrationEvent; 4 | 5 | import org.axonframework.commandhandling.gateway.CommandGateway; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.ArgumentCaptor; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.util.UUID; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.Mockito.verify; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | public class CourseRatingRecalculatedIntegrationEventHandlerTest { 20 | 21 | @Mock 22 | private CommandGateway commandGateway; 23 | 24 | @InjectMocks 25 | private CourseRatingRecalculatedIntegrationEventHandler sut; 26 | 27 | 28 | @Test 29 | void handleCourseRatingRecalculatedEvent_updateCourseRatingCommandExecuted() { 30 | // given 31 | final UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426655440001"); 32 | final CourseRatingRecalculatedIntegrationEvent event = new CourseRatingRecalculatedIntegrationEvent(uuid, 3.7); 33 | 34 | // when 35 | sut.handleCourseRatingRecalculatedEvent(event); 36 | 37 | // then 38 | final ArgumentCaptor argument = ArgumentCaptor.forClass(UpdateCourseRatingCommand.class); 39 | verify(commandGateway).send(argument.capture()); 40 | final UpdateCourseRatingCommand updateCourseRatingCommand = argument.getValue(); 41 | assertThat(updateCourseRatingCommand) 42 | .hasFieldOrPropertyWithValue("uuid", uuid) 43 | .hasFieldOrPropertyWithValue("rating", 3.7); 44 | } 45 | 46 | } 47 | --------------------------------------------------------------------------------