diff --git a/pom.xml b/pom.xml index 97f867f..f293ecf 100644 --- a/pom.xml +++ b/pom.xml @@ -18,18 +18,10 @@ 1.4.31 - - org.springframework.boot - spring-boot-starter-data-jdbc - org.springframework.boot spring-boot-starter-data-jpa - - org.springframework.boot - spring-boot-starter-jdbc - org.springframework.boot spring-boot-starter-security diff --git a/src/main/kotlin/uk/co/neviyn/projectplanner/Entities.kt b/src/main/kotlin/uk/co/neviyn/projectplanner/Entities.kt index 3a3a9e1..8b87534 100644 --- a/src/main/kotlin/uk/co/neviyn/projectplanner/Entities.kt +++ b/src/main/kotlin/uk/co/neviyn/projectplanner/Entities.kt @@ -1,11 +1,11 @@ package uk.co.neviyn.projectplanner -import com.fasterxml.jackson.annotation.JsonBackReference -import com.fasterxml.jackson.annotation.JsonManagedReference +import com.fasterxml.jackson.annotation.JsonIgnore import java.time.LocalDateTime import javax.persistence.CascadeType import javax.persistence.Entity import javax.persistence.GeneratedValue +import javax.persistence.GenerationType import javax.persistence.Id import javax.persistence.JoinColumn import javax.persistence.JoinTable @@ -14,42 +14,47 @@ import javax.persistence.ManyToOne import javax.persistence.OneToMany @Entity -class User( +open class User( var username: String = "INVALID", var email: String = "INVALID", var password: String = "INVALID", @ManyToMany(cascade = [CascadeType.ALL]) - @JsonManagedReference + @JsonIgnore @JoinTable( name = "Team", joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")], inverseJoinColumns = [JoinColumn(name = "project_id", referencedColumnName = "id")] ) - var projects: Set = mutableSetOf(), - @Id @GeneratedValue var id: Long? = null -) + var projects: MutableSet = mutableSetOf(), + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null +) {} @Entity class Project( var title: String = "INVALID", @ManyToMany(mappedBy = "projects") - @JsonBackReference - var members: Set = mutableSetOf(), + @JsonIgnore + var members: MutableSet = mutableSetOf(), @OneToMany(mappedBy = "project") - var events: Set = mutableSetOf(), - @Id @GeneratedValue var id: Long? = null + @JsonIgnore + var events: MutableSet = mutableSetOf(), + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null ) @Entity class Event( var title: String = "INVALID", var description: String = "INVALID", + var start: LocalDateTime = LocalDateTime.MIN, + var end: LocalDateTime = LocalDateTime.MIN, @ManyToOne @JoinColumn(name = "project_id") + @JsonIgnore var project: Project? = null, @ManyToMany(mappedBy = "events") - var tags: Set = mutableSetOf(), - @Id @GeneratedValue var id: Long? = null + @JsonIgnore + var tags: MutableSet = mutableSetOf(), + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null ) @Entity @@ -59,12 +64,14 @@ class Comment( var user: User? = null, @ManyToOne @JoinColumn(name = "event_id") + @JsonIgnore var event: Event? = null, @ManyToMany(mappedBy = "comments") - var tags: Set = mutableSetOf(), + @JsonIgnore + var tags: MutableSet = mutableSetOf(), var created: LocalDateTime = LocalDateTime.MIN, var comment: String = "INVALID", - @Id @GeneratedValue var id: Long? = null + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null ) @Entity @@ -76,13 +83,15 @@ class Tag( joinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")], inverseJoinColumns = [JoinColumn(name = "comment_id", referencedColumnName = "id")] ) - var comments: Set = mutableSetOf(), + @JsonIgnore + var comments: MutableSet = mutableSetOf(), @ManyToMany @JoinTable( name = "event_tags", joinColumns = [JoinColumn(name = "tag_id", referencedColumnName = "id")], inverseJoinColumns = [JoinColumn(name = "event_id", referencedColumnName = "id")] ) - var events: Set = mutableSetOf(), - @Id @GeneratedValue var id: Long? = null + @JsonIgnore + var events: MutableSet = mutableSetOf(), + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null ) \ No newline at end of file diff --git a/src/main/kotlin/uk/co/neviyn/projectplanner/HtmlController.kt b/src/main/kotlin/uk/co/neviyn/projectplanner/HtmlController.kt index 54c104d..97f584d 100644 --- a/src/main/kotlin/uk/co/neviyn/projectplanner/HtmlController.kt +++ b/src/main/kotlin/uk/co/neviyn/projectplanner/HtmlController.kt @@ -6,14 +6,37 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.ResponseBody import org.springframework.ui.Model - - +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import javax.persistence.EntityManager +import javax.transaction.Transactional @Controller -class HtmlController @Autowired constructor(val userRepository: UserRepository, val projectRepository: ProjectRepository){ +class HtmlController @Autowired constructor(val userRepository: UserRepository, val projectRepository: ProjectRepository, val entityManager: EntityManager){ + + @GetMapping("/") + fun landingPage(@AuthenticationPrincipal userDetails: CustomUserDetails?) : String { + return if(userDetails == null) "landing" + else "redirect:/projects" + } + + @GetMapping("/register") + fun register(model: Model) : String { + val user = User(username = "", email = "", password = "") + model.addAttribute("user_details", user) + return "register" + } + + @PostMapping("/register") + fun register(@ModelAttribute newUser: User) : String { + newUser.password = passwordEncoder().encode(newUser.password) + userRepository.save(newUser) + return "login" + } @GetMapping("/login") fun login(model: Model, error: String?, logout: String?): String? { @@ -22,24 +45,96 @@ class HtmlController @Autowired constructor(val userRepository: UserRepository, return "login" } - @GetMapping("/user/{id}") - @ResponseBody - fun getUser(@PathVariable id: Long) : User { - return userRepository.findById(id).get() + @GetMapping("/profile") + fun getLoggedInUser(@AuthenticationPrincipal userDetails: CustomUserDetails, model: Model) : String { + val user = DisplayUser(userDetails.user.id!!, userDetails.user.username, userDetails.user.email, "", "") + model.addAttribute("userData", user) + return "profile" } - @PreAuthorize("hasPermission(#projectID, 'Long', '')") - @GetMapping("/project/{projectID}") - @ResponseBody - fun getProject(@PathVariable projectID: Long) : Project{ - return projectRepository.findById(projectID).get() + @PostMapping("/profile") + @Transactional + fun updateLoggedInUser(@ModelAttribute userData: DisplayUser, @AuthenticationPrincipal userDetails: CustomUserDetails, model: Model) : String { + if(userData.id == userDetails.user.id!! && passwordEncoder().matches(userData.oldPassword, userDetails.password)) { + val user = userDetails.user + user.email = userData.email + if(userData.password.isNotEmpty()) user.password = passwordEncoder().encode(userData.password) + userRepository.save(user) + model.addAttribute("message", "Your profile has been updated") + } + else{ + model.addAttribute("error", "Incorrect existing password") + } + model.addAttribute("userData", DisplayUser(userData.id, userData.username, userData.email, userData.password, "")) + return "profile" } - @GetMapping("/me") - @ResponseBody - fun getLoggedInUser(@AuthenticationPrincipal userDetails: CustomUserDetails) : Long { - return userDetails.user.id!! + @PostMapping("/newproject") + fun createNewProject(@RequestBody newProject: NewProject){ + val project = Project(title = newProject.name) + projectRepository.save(project) } + @GetMapping("/projects") + @Transactional + fun listUserProjects(model: Model, @AuthenticationPrincipal userDetails: CustomUserDetails) : String { + val user = entityManager.merge(userDetails.user) // Reattach User entity + model.addAttribute("projects", user.projects) + return "projectlist" + } +} + +@Controller +@RequestMapping("/project/{id}") +class ProjectController @Autowired constructor(val projectRepository: ProjectRepository, val userRepository: UserRepository, val eventRepository: EventRepository) { + + @GetMapping("") + @PreAuthorize("hasPermission(#id, 'Long', '')") + fun getProject(@PathVariable id: Long, model: Model) : String { + val project = projectRepository.findById(id).get() + model.addAttribute("project", project) + return "project" + } + + @GetMapping("/events") + @PreAuthorize("hasPermission(#id, 'Long', '')") + fun getProjectEvents(@PathVariable id: Long) : Set { + return projectRepository.findById(id).get().events + } + + @GetMapping("/adduser") + @PreAuthorize("hasPermission(#id, 'Long', '')") + fun addUserToProjectForm(@PathVariable id: Long, model: Model) : String{ + val project = projectRepository.findById(id).get() + val users = userRepository.findByIdNotIn(project.members.map { it.id!! }).map { SimpleUser(it.id!!, it.username) } + model.addAttribute("available_users", users) + return "addprojectuser" + } + + @PostMapping("/adduser") + @PreAuthorize("hasPermission(#id, 'Long', '')") + fun addUserToProject(@PathVariable id: Long, @RequestBody u: UserID) { + val user = userRepository.findById(u.id).get() + val project = projectRepository.findById(id).get() + project.members.add(user) + projectRepository.save(project) + } + + @PostMapping("/removeuser") + @PreAuthorize("hasPermission(#id, 'Long', '')") + fun removeUserFromProject(@PathVariable id: Long, @RequestBody u: UserID) { + val user = userRepository.findById(u.id).get() + val project = projectRepository.findById(id).get() + project.members.remove(user) + projectRepository.save(project) + } + + @PostMapping("/addevent") + @PreAuthorize("hasPermission(#id, 'Long', '')") + fun addEventToProject(@PathVariable id: Long, @RequestBody e: NewEvent) { + val project = projectRepository.findById(id).get() + val event = Event(title = e.title, description = e.description, start = e.start, end = e.end, project = project) + eventRepository.save(event) + } } \ No newline at end of file diff --git a/src/main/kotlin/uk/co/neviyn/projectplanner/Repositories.kt b/src/main/kotlin/uk/co/neviyn/projectplanner/Repositories.kt index 0aee66f..1313341 100644 --- a/src/main/kotlin/uk/co/neviyn/projectplanner/Repositories.kt +++ b/src/main/kotlin/uk/co/neviyn/projectplanner/Repositories.kt @@ -4,6 +4,8 @@ import org.springframework.data.repository.CrudRepository interface UserRepository : CrudRepository{ fun findByUsername(username: String): User? + fun findByUsernameIsLike(partialUsername: String) : List + fun findByIdNotIn(ids: List) : List } interface ProjectRepository : CrudRepository{} diff --git a/src/main/kotlin/uk/co/neviyn/projectplanner/Requests.kt b/src/main/kotlin/uk/co/neviyn/projectplanner/Requests.kt new file mode 100644 index 0000000..3ac90a9 --- /dev/null +++ b/src/main/kotlin/uk/co/neviyn/projectplanner/Requests.kt @@ -0,0 +1,13 @@ +package uk.co.neviyn.projectplanner + +import java.time.LocalDateTime + +data class UserID(val id: Long) + +data class SimpleUser(val id: Long, val username: String) + +data class DisplayUser(val id: Long, val username: String, val email: String, val password: String, val oldPassword: String) + +data class NewProject(val name: String) + +data class NewEvent(val title: String, val description: String, val start: LocalDateTime, val end: LocalDateTime) \ No newline at end of file diff --git a/src/main/kotlin/uk/co/neviyn/projectplanner/Security.kt b/src/main/kotlin/uk/co/neviyn/projectplanner/Security.kt index ebcbee0..34294be 100644 --- a/src/main/kotlin/uk/co/neviyn/projectplanner/Security.kt +++ b/src/main/kotlin/uk/co/neviyn/projectplanner/Security.kt @@ -25,10 +25,10 @@ import java.io.Serializable @Configuration class SecurityConfig @Autowired constructor(val userDetailsService: UserDetailsServiceImpl) : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { - http.authorizeRequests().antMatchers("/").permitAll() + http.authorizeRequests().antMatchers("/", "/*.jpg", "/*.svg", "/register").permitAll() .anyRequest().authenticated().and() .formLogin().loginPage("/login").permitAll().and() - .logout().permitAll().and() + .logout().logoutSuccessUrl("/").permitAll().and() .httpBasic() } diff --git a/src/main/resources/static/landing_1.jpg b/src/main/resources/static/landing_1.jpg new file mode 100644 index 0000000..ac4c166 Binary files /dev/null and b/src/main/resources/static/landing_1.jpg differ diff --git a/src/main/resources/templates/addprojectuser.html b/src/main/resources/templates/addprojectuser.html new file mode 100644 index 0000000..edea7b6 --- /dev/null +++ b/src/main/resources/templates/addprojectuser.html @@ -0,0 +1,26 @@ + + + + + Add user to project | Project Planner + + + + + +
+ +
    +
    + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments.html b/src/main/resources/templates/fragments.html index b7b72e3..1bb2097 100644 --- a/src/main/resources/templates/fragments.html +++ b/src/main/resources/templates/fragments.html @@ -1,15 +1,40 @@ - + Base Title + +
    +
    + +
    + +
    \ No newline at end of file diff --git a/src/main/resources/templates/landing.html b/src/main/resources/templates/landing.html new file mode 100644 index 0000000..f32468f --- /dev/null +++ b/src/main/resources/templates/landing.html @@ -0,0 +1,33 @@ + + + + Project Planner + + +
    +
    +
    + landing page +
    +
    +
    +
    +

    Project Planner

    +
    +
    +
    +
    +

    + Manage your project deadlines. +

    +
    +
    +
    +
    + Login + Register +
    +
    +
    + + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 16cf1ef..7fdca85 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -8,24 +8,36 @@
    - \ No newline at end of file diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html new file mode 100644 index 0000000..cf2728a --- /dev/null +++ b/src/main/resources/templates/profile.html @@ -0,0 +1,56 @@ + + + + Profile | Project Planner + + +
    +
    +
    +
    +

    Profile

    +
    +
    + +
    +
    +
    + +
    + Username + +
    +
    + Email + +
    +
    + Password + +
    +

    Please enter your existing password to update your profile.

    +
    + Existing Password + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + \ No newline at end of file diff --git a/src/main/resources/templates/project.html b/src/main/resources/templates/project.html new file mode 100644 index 0000000..55007bd --- /dev/null +++ b/src/main/resources/templates/project.html @@ -0,0 +1,96 @@ + + + + Project Planner + + + + + + + + +
    +
    +
    +
    +

    Project Name

    +
    +
    +
    +
    +
    + +
    +
    +
    Members:
    +
    +
    +
    +

    username

    + + + +
    +
    +
    + Add member +
    +
    +
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/src/main/resources/templates/projectlist.html b/src/main/resources/templates/projectlist.html new file mode 100644 index 0000000..4d65d95 --- /dev/null +++ b/src/main/resources/templates/projectlist.html @@ -0,0 +1,28 @@ + + + + Projects | Project Planner + + +
    + +
    +
    +
    +
    +
    +

    My Projects

    +
    +
    +
    +
    +
    + + Title + +
    +
    +
    +
    + + \ No newline at end of file diff --git a/src/main/resources/templates/register.html b/src/main/resources/templates/register.html new file mode 100644 index 0000000..daeacf4 --- /dev/null +++ b/src/main/resources/templates/register.html @@ -0,0 +1,64 @@ + + + + Register | Project Planner + + +
    +
    +
    +

    Create Account

    +
    +
    +
    +
    +
    +
    + + +
    + Please enter an email address. +
    +
    +
    + + +
    + Please enter a username. +
    +
    +
    + + +
    + Please enter a password. +
    +
    + +
    +
    +
    +
    + + + \ No newline at end of file