diff --git a/forum.nim b/forum.nim index 924d657..4084888 100644 --- a/forum.nim +++ b/forum.nim @@ -953,6 +953,20 @@ proc selectLikes(postId: int): seq[User] = for row in getAllRows(db, likeQuery, $postId): result.add(selectUser(row)) +proc selectThreadAuthor(threadId: int): User = + const authorQuery = + sql""" + select name, email, strftime('%s', lastOnline), status + from person where id in ( + select author from post + where thread = ? + order by id + limit 1 + ) + """ + + return selectUser(getRow(db, authorQuery, threadId)) + proc selectThread(threadRow: seq[string]): Thread = const postsQuery = sql"""select count(*), strftime('%s', creation) from post @@ -964,16 +978,6 @@ proc selectThread(threadRow: seq[string]): Thread = from person u, post p where p.author = u.id and p.thread = ? group by name order by count(*) desc limit 5; """ - const authorQuery = - sql""" - select name, email, strftime('%s', lastOnline), status - from person where id in ( - select author from post - where thread = ? - order by id - limit 1 - ) - """ let posts = getRow(db, postsQuery, threadRow[0]) @@ -1000,7 +1004,7 @@ proc selectThread(threadRow: seq[string]): Thread = thread.users.add(selectUser(user)) # Grab the author. - thread.author = selectUser(getRow(db, authorQuery, thread.id)) + thread.author = selectThreadAuthor(thread.id) return thread @@ -1220,6 +1224,29 @@ proc executeUnlike(c: TForumData, postId: int) = # Delete the like. exec(db, crud(crDelete, "like"), likeId) +proc executeDeletePost(c: TForumData, postId: int) = + # Verify that this post belongs to the user. + const postQuery = sql""" + select p.id from post p + where p.author = ? and p.id = ? + """ + let id = getValue(db, postQuery, postId, c.username) + + if id.len == 0 and c.rank < Admin: + raise newForumError("You cannot delete this post") + + # Set the `isDeleted` flag. + exec(db, crud(crUpdate, "post", "isDeleted"), "1", postId) + +proc executeDeleteThread(c: TForumData, threadId: int) = + # Verify that this thread belongs to the user. + let author = selectThreadAuthor(threadId) + if author.name != c.username and c.rank < Admin: + raise newForumError("You cannot delete this thread") + + # Set the `isDeleted` flag. + exec(db, crud(crUpdate, "thread", "isDeleted"), "1", threadId) + initialise() routes: @@ -1645,6 +1672,33 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post re"/delete(Post|Thread)": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "id" in formData + + let id = getInt(formData["id"].body, -1) + cond id != -1 + + try: + case request.path + of "/deletePost": + executeDeletePost(c, id) + of "/deleteThread": + executeDeleteThread(c, id) + else: + assert false + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + get "/t/@id": cond "id" in request.params diff --git a/frontend/delete.nim b/frontend/delete.nim new file mode 100644 index 0000000..789aebc --- /dev/null +++ b/frontend/delete.nim @@ -0,0 +1,132 @@ +when defined(js): + import sugar, httpcore, options, json + import dom except Event + + include karax/prelude + import karax / [kajax, kdom] + + import error, post, threadlist, user + import karaxutils + + type + DeleteKind* = enum + DeleteUser, DeletePost, DeleteThread + + DeleteModal* = ref object + shown: bool + loading: bool + onDeletePost: proc (post: Post) + onDeleteThread: proc (thread: Thread) + onDeleteUser: proc (user: User) + error: Option[PostError] + case kind: DeleteKind + of DeleteUser: + user: User + of DeletePost: + post: Post + of DeleteThread: + thread: Thread + + proc onDeletePost(httpStatus: int, response: kstring, state: DeleteModal) = + postFinished: + state.shown = false + case state.kind + of DeleteUser: + state.onDeleteUser(state.user) + of DeletePost: + state.onDeletePost(state.post) + of DeleteThread: + state.onDeleteThread(state.thread) + + proc onDelete(ev: Event, n: VNode, state: DeleteModal) = + state.loading = true + state.error = none[PostError]() + + let uri = + case state.kind + of DeleteUser: + makeUri("/deleteUser") + of DeleteThread: + makeUri("/deleteThread") + of DeletePost: + makeUri("/deletePost") + # TODO: This is a hack, karax should support this. + let formData = newFormData() + case state.kind + of DeleteUser: + formData.append("username", state.user.name) + of DeletePost: + formData.append("id", $state.post.id) + of DeleteThread: + formData.append("id", $state.thread.id) + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onDeletePost(s, r, state)) + + proc onClose(ev: Event, n: VNode, state: DeleteModal) = + state.shown = false + ev.preventDefault() + + proc newDeleteModal*( + onDeletePost: proc (post: Post), + onDeleteThread: proc (thread: Thread), + onDeleteUser: proc (user: User), + ): DeleteModal = + DeleteModal( + shown: false, + onDeletePost: onDeletePost, + onDeleteThread: onDeleteThread, + onDeleteUser: onDeleteUser, + ) + + proc show*(state: DeleteModal, thing: User | Post | Thread) = + state.shown = true + state.error = none[PostError]() + when thing is User: + state.kind = DeleteUser + state.user = thing + when thing is Post: + state.kind = DeletePost + state.post = thing + when thing is Thread: + state.kind = DeleteThread + state.thread = thing + + proc render*(state: DeleteModal): VNode = + result = buildHtml(): + tdiv(class=class({"active": state.shown}, "modal modal-sm"), + id="login-modal"): + a(href="", class="modal-overlay", "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-container"): + tdiv(class="modal-header"): + a(href="", class="btn btn-clear float-right", + "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-title h5"): + text "Delete" + tdiv(class="modal-body"): + tdiv(class="content"): + p(): + text "Are you sure you want to delete this " + case state.kind + of DeleteUser: + text "user account?" + of DeleteThread: + text "thread?" + of DeletePost: + text "post?" + tdiv(class="modal-footer"): + if state.error.isSome(): + p(class="text-error"): + text state.error.get().message + + button(class=class( + {"loading": state.loading}, + "btn btn-primary" + ), + onClick=(ev: Event, n: VNode) => onDelete(ev, n, state)): + italic(class="fas fa-trash-alt") + text " Delete" + button(class="btn", + onClick=(ev: Event, n: VNode) => (state.shown = false)): + text "Cancel" \ No newline at end of file diff --git a/frontend/postlist.nim b/frontend/postlist.nim index 13c91d3..68f2e85 100644 --- a/frontend/postlist.nim +++ b/frontend/postlist.nim @@ -1,5 +1,6 @@ import options, json, times, httpcore, strformat, sugar, math, strutils +import sequtils import threadlist, category, post, user type @@ -17,7 +18,7 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, error, replybox, editbox, postbutton + import karaxutils, error, replybox, editbox, postbutton, delete type State = ref object @@ -29,10 +30,13 @@ when defined(js): editing: Option[Post] ## If in edit mode, this contains the post. editBox: EditBox likeButton: LikeButton + deleteModal: DeleteModal proc onReplyPosted(id: int) proc onEditPosted(id: int, content: string, subject: Option[string]) proc onEditCancelled() + proc onDeletePost(post: Post) + proc onDeleteThread(thread: Thread) proc newState(): State = State( list: none[PostList](), @@ -41,7 +45,8 @@ when defined(js): replyingTo: none[Post](), replyBox: newReplyBox(onReplyPosted), editBox: newEditBox(onEditPosted, onEditCancelled), - likeButton: newLikeButton() + likeButton: newLikeButton(), + deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil) ) var @@ -137,6 +142,21 @@ when defined(js): # TODO: Ensure the edit box is as big as its content. Auto resize the # text area. + proc onDeletePost(post: Post) = + state.list.get().posts.keepIf( + x => x.id != post.id + ) + + proc onDeleteThread(thread: Thread) = + window.location.href = makeUri("/") + + proc onDeleteClick(e: Event, n: VNode, p: Post) = + let list = state.list.get() + if list.posts[0].id == p.id: + state.deleteModal.show(list.thread) + else: + state.deleteModal.show(p) + proc onLoadMore(ev: Event, n: VNode, start: int, post: Post) = loadMore(start, post.moreBefore) # TODO: Don't load all! @@ -173,7 +193,8 @@ when defined(js): onEditClick(e, n, post)): button(class="btn"): italic(class="far fa-edit") - tdiv(class="delete-button"): + tdiv(class="delete-button", + onClick=(e: Event, n: VNode) => onDeleteClick(e, n, post)): button(class="btn"): italic(class="far fa-trash-alt") @@ -338,4 +359,6 @@ when defined(js): italic(class="fas fa-reply") text " Reply" - render(state.replyBox, list.thread, state.replyingTo, false) \ No newline at end of file + render(state.replyBox, list.thread, state.replyingTo, false) + + render(state.deleteModal) \ No newline at end of file