diff --git a/pages/admin/tag/[id].tsx b/pages/admin/tag/[id].tsx index b77dbb2a..13264484 100644 --- a/pages/admin/tag/[id].tsx +++ b/pages/admin/tag/[id].tsx @@ -67,7 +67,7 @@ function PostEditPage() { />} > - + {editMode === EditMode.Edit ? { return (
- + {step === 1 ? <> CREATE YOUR ACCOUNT 🤘 Let's start your {config.appName} journey diff --git a/pages/tags/[name].tsx b/pages/tags/[name].tsx index 5fca26c1..59029268 100644 --- a/pages/tags/[name].tsx +++ b/pages/tags/[name].tsx @@ -1,4 +1,4 @@ -import { Box, Button, Flex, Heading, HStack, Image, Text, VStack } from "@chakra-ui/react" +import { Avatar, Box, Button, Flex, Heading, HStack, Image, Text, useToast, VStack } from "@chakra-ui/react" import Card from "components/card" import Empty from "components/empty" import { MarkdownRender } from "components/markdown-editor/render" @@ -17,16 +17,24 @@ import { isAdmin } from "utils/role" import Follow from "components/interaction/follow" import Count from "components/count" import StoryFilters from "components/story/story-filter" +import { UserSimple } from "src/types/user" +import Users from "components/users/users" +import Head from "next/head" +import { getUserName } from "utils/user" +import Link from "next/link" +import { getSvgIcon } from "components/svg-icon" const UserPage = () => { const router = useRouter() + const toast = useToast() const [tag, setTag]: [Tag, any] = useState(null) - + const [moderators,setModerators]:[UserSimple[],any] = useState([]) const [followed, setFollowed] = useState(null) useEffect(() => { if (tag) { requestApi.get(`/interaction/followed/${tag.id}`).then(res => setFollowed(res.data)) + requestApi.get(`/tag/moderators/${tag.id}`).then(res => setModerators(res.data)) } }, [tag]) @@ -51,6 +59,30 @@ const UserPage = () => { }, [router.query.name]) const session = useSession() + + const isModerator = () => { + if (isAdmin(session.user.role)) { + return true + } + + for (const m of moderators) { + if (m.id === session.user.id) { + return true + } + } + + return false + } + + const removeStory = async id => { + await requestApi.delete(`/tag/story/${tag.id}/${id}`) + toast({ + description: "从标签移除成功,刷新页面可看到效果", + status: "success", + duration: 3000, + isClosable: true, + }) + } return ( <> { {followed !== null && } - {isAdmin(session?.user.role) && } + {isModerator() && } @@ -80,8 +112,8 @@ const UserPage = () => { - {tag.id && - + {tag.id && + } @@ -101,9 +133,22 @@ const UserPage = () => { - About this tag + About this tag + + {moderators.length > 0 && + Tag moderators + + {moderators.map(m => + + + {getUserName(m)} + + )} + + + } } diff --git a/server/internal/api/tag.go b/server/internal/api/tag.go index 31cd0daa..2949965f 100644 --- a/server/internal/api/tag.go +++ b/server/internal/api/tag.go @@ -63,12 +63,6 @@ func GetTagsByIDs(c *gin.Context) { } func SubmitTag(c *gin.Context) { - user := user.CurrentUser(c) - if !user.Role.IsAdmin() { - c.JSON(http.StatusForbidden, common.RespError(e.NoEditorPermission)) - return - } - tag := &models.Tag{} err := c.Bind(&tag) if err != nil { @@ -76,6 +70,12 @@ func SubmitTag(c *gin.Context) { return } + user := user.CurrentUser(c) + if !tags.IsModerator(tag.ID, user) { + c.JSON(http.StatusForbidden, common.RespError(e.NoEditorPermission)) + return + } + tag.Creator = user.ID err1 := tags.SubmitTag(tag) if err1 != nil { @@ -157,6 +157,7 @@ func AddModerator(c *gin.Context) { user := user.CurrentUser(c) if !user.Role.IsSuperAdmin() { c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) + return } err := tags.AddModerator(req.TagID, req.Username) @@ -188,3 +189,27 @@ func DeleteModerator(c *gin.Context) { c.JSON(http.StatusOK, common.RespSuccess(nil)) } + +func RemoveTagStory(c *gin.Context) { + tagID := c.Param("tagID") + storyID := c.Param("storyID") + if tagID == "" || storyID == "" { + c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid)) + return + } + + user := user.CurrentUser(c) + if !tags.IsModerator(tagID, user) { + c.JSON(http.StatusForbidden, common.RespError(e.NoEditorPermission)) + return + } + + err := tags.RemoveTagStory(tagID, storyID) + if err != nil { + c.JSON(err.Status, common.RespError(err.Message)) + return + } + + c.JSON(http.StatusOK, common.RespSuccess(nil)) + +} diff --git a/server/internal/config.go b/server/internal/config.go index 0b8e8159..be51a400 100644 --- a/server/internal/config.go +++ b/server/internal/config.go @@ -6,9 +6,11 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/imdotdev/im.dev/server/internal/user" "github.com/imdotdev/im.dev/server/pkg/common" "github.com/imdotdev/im.dev/server/pkg/config" "github.com/imdotdev/im.dev/server/pkg/db" + "github.com/imdotdev/im.dev/server/pkg/e" ) type Config struct { @@ -58,6 +60,12 @@ func UpdateConfig(c *gin.Context) { d := make(map[string]interface{}) c.Bind(&d) + currentUser := user.CurrentUser(c) + if !currentUser.Role.IsAdmin() { + c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) + return + } + b, _ := json.Marshal(&d) _, err := db.Conn.Exec(`UPDATE config SET data=?,updated=? WHERE id=?`, b, time.Now(), 1) if err != nil { diff --git a/server/internal/server.go b/server/internal/server.go index da88a35b..21974ebf 100644 --- a/server/internal/server.go +++ b/server/internal/server.go @@ -104,6 +104,8 @@ func (s *Server) Start() error { r.GET("/tag/moderators/:id", api.GetTagModerators) r.POST("/tag/moderator", IsLogin(), api.AddModerator) r.DELETE("/tag/moderator/:tagID/:userID", IsLogin(), api.DeleteModerator) + r.DELETE("/tag/story/:tagID/:storyID", IsLogin(), api.RemoveTagStory) + // user apis r.GET("/user/all", api.GetUsers) r.POST("/user/ids", api.GetUsersByIDs) diff --git a/server/internal/tags/tags.go b/server/internal/tags/tags.go index e479b471..da2df2b5 100644 --- a/server/internal/tags/tags.go +++ b/server/internal/tags/tags.go @@ -311,3 +311,27 @@ func DeleteModerator(tagID, userID string) *e.Error { return nil } + +func IsModerator(tagID string, user *models.User) bool { + if user.Role.IsAdmin() { + return true + } + + var uid string + db.Conn.QueryRow("SELECT user_id FROM tag_moderators WHERE tag_id=? and user_id=?", tagID, user.ID).Scan(&uid) + if uid == user.ID { + return true + } + + return false +} + +func RemoveTagStory(tagID, storyID string) *e.Error { + _, err := db.Conn.Exec("DELETE FROM tags_using WHERE tag_id=? and target_id=?", tagID, storyID) + if err != nil { + logger.Warn("remove tag story error", "error", err) + return e.New(http.StatusInternalServerError, e.Internal) + } + + return nil +} diff --git a/src/components/story/stories.tsx b/src/components/story/stories.tsx index 8671b5b3..8fe019b4 100644 --- a/src/components/story/stories.tsx +++ b/src/components/story/stories.tsx @@ -17,11 +17,12 @@ interface Props { showOrg?: boolean onLoad?: any filter?: string + onRemove?: any } export const Stroies = (props: Props) => { - const { card = StoryCard, showFooter = true, type = "classic", showPinned = false, showOrg = true, onLoad, filter } = props + const { card = StoryCard, showFooter = true, type = "classic", showPinned = false, showOrg = true, onLoad, filter,onRemove } = props const [posts, setPosts] = useState([]) const [noMore, setNoMore] = useState(false) @@ -93,7 +94,7 @@ export const Stroies = (props: Props) => { {posts.map((story, i) => - + )} {isFetching && 'Fetching more list items...'} diff --git a/src/components/story/story-card.tsx b/src/components/story/story-card.tsx index 4d89d5d4..b2046aa3 100644 --- a/src/components/story/story-card.tsx +++ b/src/components/story/story-card.tsx @@ -1,5 +1,5 @@ import React from "react" -import { Box, Heading, HStack, Image, Tag, Text, useMediaQuery, VStack } from "@chakra-ui/react" +import { Box, Flex, Heading, HStack, Image, Tag, Text, useMediaQuery, VStack, AlertDialog, AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogBody, AlertDialogFooter, Button } from "@chakra-ui/react" import { Story } from "src/types/story" import StoryAuthor from "./story-author" import Link from "next/link" @@ -9,7 +9,6 @@ import { getSvgIcon } from "components/svg-icon" import Count from "components/count" import Highlighter from 'react-highlight-words'; import { IDType } from "src/types/id" -import { ReserveUrls } from "src/data/reserve-urls" import { getCommentsUrl, getStoryUrl } from "utils/story" interface Props { @@ -17,6 +16,7 @@ interface Props { type?: string highlight?: string showOrg?: boolean + onRemove?: any } @@ -25,47 +25,85 @@ export const StoryCard = (props: Props) => { const [isLargeScreen] = useMediaQuery("(min-width: 768px)") const Layout = isLargeScreen ? HStack : VStack + const [isOpen, setIsOpen] = React.useState(false) + const onClose = () => setIsOpen(false) + const cancelRef = React.useRef() + return ( - - - - - - - - {story.type === IDType.Series && SERIES} - {story.pinned && 置顶} - - {type !== "classic" && {story.rawTags.map(t => #{t.name})}} - - - - {story.cover && type === "classic" && } - - + <> + + + + {props.onRemove && setIsOpen(true)}>{getSvgIcon("close", "1.1rem")}} + + + + + + + {story.type === IDType.Series && SERIES} + {story.pinned && 置顶} + + + - - - - - {getSvgIcon("comments", "1.3rem")} - - + {type !== "classic" && {story.rawTags.map(t => #{t.name})}} + + + + {story.cover && type === "classic" && } + + + + + + {getSvgIcon("comments", "1.3rem")} + + + + + + + + + + + + + + 移除 - {story.title} + + + Are you sure? You can't undo this action afterwards. + - - - + + + + + + + + ) }