feat: 投稿フォームを追加

This commit is contained in:
usbharu 2024-08-13 12:31:02 +09:00
parent 1fc6a1fa38
commit 91573c0b2e
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
11 changed files with 119 additions and 11 deletions

View File

@ -1,7 +1,6 @@
package dev.usbharu.hideout.core.application.post package dev.usbharu.hideout.core.application.post
import dev.usbharu.hideout.core.application.exception.InternalServerException import dev.usbharu.hideout.core.application.exception.InternalServerException
import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.actor.Actor import dev.usbharu.hideout.core.domain.model.actor.Actor
@ -32,7 +31,7 @@ class GetPostDetailApplicationService(
val post = postRepository.findById(PostId(command.postId)) val post = postRepository.findById(PostId(command.postId))
?: throw IllegalArgumentException("Post ${command.postId} not found.") ?: throw IllegalArgumentException("Post ${command.postId} not found.")
if (iPostReadAccessControl.isAllow(post, principal).not()) { if (iPostReadAccessControl.isAllow(post, principal).not()) {
throw PermissionDeniedException() throw IllegalArgumentException("Post ${command.postId} not found.")
} }
val actor = val actor =
actorRepository.findById(post.actorId) ?: throw InternalServerException("Actor ${post.actorId} not found.") actorRepository.findById(post.actorId) ?: throw InternalServerException("Actor ${post.actorId} not found.")
@ -65,7 +64,7 @@ class GetPostDetailApplicationService(
val post = postRepository.findById(postId) ?: return null val post = postRepository.findById(postId) ?: return null
if (iPostReadAccessControl.isAllow(post, principal).not()) { if (iPostReadAccessControl.isAllow(post, principal).not()) {
throw PermissionDeniedException() return null
} }
val (first, second: Instance, third) = if (actor.id != post.actorId) { val (first, second: Instance, third) = if (actor.id != post.actorId) {

View File

@ -83,6 +83,8 @@ class SecurityConfig {
authorize(POST, "/auth/sign_up", permitAll) authorize(POST, "/auth/sign_up", permitAll)
authorize(GET, "/users/{username}/posts/{postId}", permitAll) authorize(GET, "/users/{username}/posts/{postId}", permitAll)
authorize(GET, "/files/*", permitAll) authorize(GET, "/files/*", permitAll)
authorize(POST, "/publish", authenticated)
authorize(GET, "/publish", authenticated)
authorize(anyRequest, authenticated) authorize(anyRequest, authenticated)
} }

View File

@ -16,6 +16,12 @@ interface IPostReadAccessControl {
class DefaultPostReadAccessControl(private val relationshipRepository: RelationshipRepository) : class DefaultPostReadAccessControl(private val relationshipRepository: RelationshipRepository) :
IPostReadAccessControl { IPostReadAccessControl {
override suspend fun isAllow(post: Post, principal: Principal): Boolean { override suspend fun isAllow(post: Post, principal: Principal): Boolean {
//ポスト主は無条件で見れる
if (post.actorId == principal.actorId) {
return true
}
val relationship = (relationshipRepository.findByActorIdAndTargetId(post.actorId, principal.actorId) val relationship = (relationshipRepository.findByActorIdAndTargetId(post.actorId, principal.actorId)
?: Relationship.default(post.actorId, principal.actorId)) ?: Relationship.default(post.actorId, principal.actorId))

View File

@ -1,9 +1,11 @@
package dev.usbharu.hideout.core.interfaces.web.posts package dev.usbharu.hideout.core.interfaces.web.posts
import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService
import dev.usbharu.hideout.core.application.post.GetPostDetail import dev.usbharu.hideout.core.application.post.GetPostDetail
import dev.usbharu.hideout.core.application.post.GetPostDetailApplicationService import dev.usbharu.hideout.core.application.post.GetPostDetailApplicationService
import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder
import org.springframework.security.access.AccessDeniedException
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.ui.Model import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
@ -18,10 +20,15 @@ class PostsController(
@GetMapping("/users/{name}/posts/{id}") @GetMapping("/users/{name}/posts/{id}")
suspend fun postById(@PathVariable id: Long, model: Model): String { suspend fun postById(@PathVariable id: Long, model: Model): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal() val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
try {
val post = getPostDetailApplicationService.execute(GetPostDetail(id), principal) val post = getPostDetailApplicationService.execute(GetPostDetail(id), principal)
val instance = getLocalInstanceApplicationService.execute(Unit, principal) val instance = getLocalInstanceApplicationService.execute(Unit, principal)
model.addAttribute("post", post) model.addAttribute("post", post)
model.addAttribute("instance", instance) model.addAttribute("instance", instance)
} catch (e: PermissionDeniedException) {
throw AccessDeniedException("403 Forbidden", e)
}
return "postById" return "postById"
} }
} }

View File

@ -3,17 +3,23 @@ package dev.usbharu.hideout.core.interfaces.web.posts
import dev.usbharu.hideout.core.application.actor.GetUserDetail import dev.usbharu.hideout.core.application.actor.GetUserDetail
import dev.usbharu.hideout.core.application.actor.GetUserDetailApplicationService import dev.usbharu.hideout.core.application.actor.GetUserDetailApplicationService
import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService
import dev.usbharu.hideout.core.application.post.RegisterLocalPost
import dev.usbharu.hideout.core.application.post.RegisterLocalPostApplicationService
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder
import org.springframework.security.access.AccessDeniedException import org.springframework.security.access.AccessDeniedException
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.ui.Model import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PostMapping
@Controller @Controller
class PublishController( class PublishController(
private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService, private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService,
private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder, private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder,
private val getUserDetailApplicationService: GetUserDetailApplicationService private val getUserDetailApplicationService: GetUserDetailApplicationService,
private val userRegisterLocalPostApplicationService: RegisterLocalPostApplicationService
) { ) {
@GetMapping("/publish") @GetMapping("/publish")
suspend fun publish(model: Model): String { suspend fun publish(model: Model): String {
@ -26,7 +32,29 @@ class PublishController(
val instance = getLocalInstanceApplicationService.execute(Unit, principal) val instance = getLocalInstanceApplicationService.execute(Unit, principal)
val userDetail = getUserDetailApplicationService.execute(GetUserDetail(principal.userDetailId!!.id), principal) val userDetail = getUserDetailApplicationService.execute(GetUserDetail(principal.userDetailId!!.id), principal)
model.addAttribute("instance", instance) model.addAttribute("instance", instance)
model.addAttribute("user") model.addAttribute("user", userDetail)
model.addAttribute("form", PublishPost())
return "post-postForm" return "post-postForm"
} }
@PostMapping("/publish")
suspend fun publishForm(@ModelAttribute publishPost: PublishPost): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
if (principal.userDetailId == null) {
throw AccessDeniedException("403 Forbidden")
}
val command = RegisterLocalPost(
content = publishPost.status.orEmpty(),
overview = publishPost.overview,
visibility = Visibility.valueOf(publishPost.visibility.uppercase()),
repostId = null,
replyId = null,
sensitive = false,
mediaIds = emptyList()
)
val id = userRegisterLocalPostApplicationService.execute(command, principal)
return "redirect:/users/${principal.acct?.userpart}/posts/${id}"
}
} }

View File

@ -0,0 +1,3 @@
package dev.usbharu.hideout.core.interfaces.web.posts
data class PublishPost(var status: String? = null, var overview: String? = null, var visibility: String = "PUBLIC")

View File

@ -5,7 +5,15 @@ common.thumbnail=\u30B5\u30E0\u30CD\u30A4\u30EB
common.unknwon-file-type=\u4E0D\u660E\u306A\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F common.unknwon-file-type=\u4E0D\u660E\u306A\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F
common.video=\u52D5\u753B common.video=\u52D5\u753B
common.video-download-link=\u52D5\u753B\u30D5\u30A1\u30A4\u30EB\u307E\u305F\u306F\u30B5\u30E0\u30CD\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9 common.video-download-link=\u52D5\u753B\u30D5\u30A1\u30A4\u30EB\u307E\u305F\u306F\u30B5\u30E0\u30CD\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9
common.visibility=\u516C\u958B\u7BC4\u56F2
common.visibility-followers=\u30D5\u30A9\u30ED\u30EF\u30FC\u306E\u307F
common.visibility-public=\u30D1\u30D6\u30EA\u30C3\u30AF
common.visibility-unlisted=\u672A\u53CE\u8F09
post-by-id.title={0} \u3055\u3093\u306E\u6295\u7A3F - {1} post-by-id.title={0} \u3055\u3093\u306E\u6295\u7A3F - {1}
post-form.new-posts=\u65B0\u3057\u3044\u6295\u7A3F! post-form.new-posts=\u65B0\u3057\u3044\u6295\u7A3F!
post-form.new-posts-cw=CW
post-form.new-posts-cw-title=\u30B3\u30F3\u30C6\u30F3\u30C4\u306B\u95B2\u89A7\u6CE8\u610F\u3092\u3064\u3051\u308B
post-form.new-posts-form-label=\u4ECA\u306A\u306B\u3057\u3066\u308B?
post-form.new-posts-submit=\u6295\u7A3F\u3059\u308B
post.repost=\u30EA\u30DD\u30B9\u30C8 post.repost=\u30EA\u30DD\u30B9\u30C8
post.repost-by={0}\u304C\u30EA\u30DD\u30B9\u30C8 post.repost-by={0}\u304C\u30EA\u30DD\u30B9\u30C8

View File

@ -5,6 +5,14 @@ common.thumbnail=thumbnail
common.unknwon-file-type=Unknown filetype common.unknwon-file-type=Unknown filetype
common.video=Video common.video=Video
common.video-download-link=Download the Video or thumbnail. common.video-download-link=Download the Video or thumbnail.
common.visibility=Visibility
common.visibility-followers=Followers only
common.visibility-public=Public
common.visibility-unlisted=Unlisted
post-form.new-posts=New Posts! post-form.new-posts=New Posts!
post-form.new-posts-cw=CW
post-form.new-posts-cw-title=Add content warning
post-form.new-posts-form-label=What's on your mind?
post-form.new-posts-submit=Submit!
post.repost=Repost post.repost=Repost
post.repost-by=Repost by {0} post.repost-by=Repost by {0}

View File

@ -5,7 +5,15 @@ common.thumbnail=\u30B5\u30E0\u30CD\u30A4\u30EB
common.unknwon-file-type=\u4E0D\u660E\u306A\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F common.unknwon-file-type=\u4E0D\u660E\u306A\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F
common.video=\u52D5\u753B common.video=\u52D5\u753B
common.video-download-link=\u52D5\u753B\u30D5\u30A1\u30A4\u30EB\u307E\u305F\u306F\u30B5\u30E0\u30CD\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9 common.video-download-link=\u52D5\u753B\u30D5\u30A1\u30A4\u30EB\u307E\u305F\u306F\u30B5\u30E0\u30CD\u30A4\u30EB\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9
common.visibility=\u516C\u958B\u7BC4\u56F2
common.visibility-followers=\u30D5\u30A9\u30ED\u30EF\u30FC\u306E\u307F
common.visibility-public=\u30D1\u30D6\u30EA\u30C3\u30AF
common.visibility-unlisted=\u672A\u53CE\u8F09
post-by-id.title={0} \u3055\u3093\u306E\u6295\u7A3F - {1} post-by-id.title={0} \u3055\u3093\u306E\u6295\u7A3F - {1}
post-form.new-posts=\u65B0\u3057\u3044\u6295\u7A3F! post-form.new-posts=\u65B0\u3057\u3044\u6295\u7A3F!
post-form.new-posts-cw=CW
post-form.new-posts-cw-title=\u30B3\u30F3\u30C6\u30F3\u30C4\u306B\u95B2\u89A7\u6CE8\u610F\u3092\u3064\u3051\u308B
post-form.new-posts-form-label=\u4ECA\u306A\u306B\u3057\u3066\u308B?
post-form.new-posts-submit=\u6295\u7A3F\u3059\u308B
post.repost=\u30EA\u30DD\u30B9\u30C8 post.repost=\u30EA\u30DD\u30B9\u30C8
post.repost-by={0}\u304C\u30EA\u30DD\u30B9\u30C8 post.repost-by={0}\u304C\u30EA\u30DD\u30B9\u30C8

View File

@ -1,10 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Title</title> <title>Title</title>
</head> </head>
<body> <body>
<div th:fragment="single-simple-actor(actor)"></div>
</body> </body>
</html> </html>

View File

@ -8,5 +8,44 @@
</head> </head>
<body> <body>
<noscript>
<div>
<img alt="" height="80px" src="" th:src="${user.iconUrl}" width="80px">
<div style="display: inline-block">
<p th:text="${user.screenName}+'('+${user.name}+'@'+${user.domain}+')'"></p>
</div>
</div>
<form action="/publish" method="post" th:action="@{/publish}" th:object="${form}">
<div>
<label for="stats-form-overview" th:text="#{post-form.new-posts-cw}"
th:title="#{post-form.new-posts-cw-title}"
title="Add content warning">CW</label>
<input id="stats-form-overview" name="overview" type="text">
</div>
<div>
<label for="status-form" th:text="#{post-form.new-posts-form-label}">What's on your mind?</label>
<br>
<textarea cols="33" id="status-form" name="status" rows="5"></textarea>
</div>
<div>
<fieldset>
<legend th:text="#{common.visibility}">Visibility</legend>
<label for="status-form-visibility-public" th:text="#{common.visibility-public}">Public</label>
<input id="status-form-visibility-public" name="visibility" th:checked="${form.visibility == 'PUBLIC'}" type="radio"
value="PUBLIC">
<label for="status-form-visibility-unlisted" th:text="#{common.visibility-unlisted}">Unlisted</label>
<input id="status-form-visibility-unlisted" name="visibility" th:checked="${form.visibility == 'UNLISTED'}" type="radio"
value="UNLISTED">
<label for="status-form-visibility-followers" th:text="#{common.visibility-followers}">Followers</label>
<input id="status-form-visibility-followers" name="visibility" th:checked="${form.visibility == 'FOLLOWERS'}" type="radio"
value="FOLLOWERS">
</fieldset>
</div>
<div>
<input th:value="#{post-form.new-posts-submit}" type="submit" value="Publish!">
</div>
</form>
</noscript>
</body> </body>
</html> </html>