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.
+
-
-
-
+
+
+
+
+
+
+
+ >
)
}