diff --git a/pages/editor/series.tsx b/pages/editor/series.tsx index 52bea20f..dd98e990 100644 --- a/pages/editor/series.tsx +++ b/pages/editor/series.tsx @@ -25,6 +25,21 @@ var validator = require('validator'); const newSeries: Story = { title: '', brief: '', cover: '', type: IDType.Series } const SeriesPage = () => { + return ( + <> + + + + + + + + ) +} +export default SeriesPage + + +export const SeriesEditor = ({orgID=""}) => { const [currentSeries, setCurrentSeries]: [Story, any] = useState(null) const [series, setSeries] = useState([]) const [posts, setPosts] = useState([]) @@ -34,12 +49,26 @@ const SeriesPage = () => { const borderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) - const getSeries = () => { - requestApi.get(`/story/posts/editor?type=${IDType.Series}`).then((res) => setSeries(res.data)).catch(_ => setPosts([])) + const getSeries = async () => { + let res + if (orgID) { + res = await requestApi.get(`/story/posts/org/${orgID}?type=${IDType.Series}`) + } else { + res = await requestApi.get(`/story/posts/editor?type=${IDType.Series}`) + } + + setSeries(res.data) } - const getPosts = () => { - requestApi.get(`/story/posts/editor?type=${IDType.Post}`).then((res) => setPosts(res.data)).catch(_ => setPosts([])) + const getPosts = async () => { + let res + if (orgID) { + res = await requestApi.get(`/story/posts/org/${orgID}?type=${IDType.Post}`) + } else { + res = await requestApi.get(`/story/posts/editor?type=${IDType.Post}`) + } + + setPosts(res.data) } useEffect(() => { @@ -79,9 +108,13 @@ const SeriesPage = () => { const submitSeries = async (values, _) => { // 这里必须按照顺序同步提交 - await requestApi.post(`/story`, values) - await requestApi.post(`/story/series/post/${values.id}`, seriesPosts) + if (orgID) { + await requestApi.post(`/story`, {...values,ownerID: orgID}) + } else { + await requestApi.post(`/story`, values) + } + await requestApi.post(`/story/series/post/${values.id}`, seriesPosts) toast({ description: "提交成功", @@ -147,15 +180,16 @@ const SeriesPage = () => { } const onPinPost = async id => { - await requestApi.post(`/story/pin/${id}`) + if (orgID) { + await requestApi.post(`/org/pin/story/${id}`) + } else { + await requestApi.post(`/story/pin/${id}`) + } + getSeries() } return ( - <> - - - {currentSeries ? <> @@ -293,10 +327,5 @@ const SeriesPage = () => { } } - - - ) -} -export default SeriesPage - +} \ No newline at end of file diff --git a/pages/settings/navbar.tsx b/pages/settings/navbar.tsx index b0028e2f..830195d3 100644 --- a/pages/settings/navbar.tsx +++ b/pages/settings/navbar.tsx @@ -13,7 +13,22 @@ import { IDType } from "src/types/id" import { Story } from "src/types/story" const UserNavbarPage = () => { - const [navbars, setNavbars]:[Navbar[],any] = useState([]) + return ( + <> + + + + + + + + ) +} +export default UserNavbarPage + + +export const NavbarEditor = ({ orgID = "" }) => { + const [navbars, setNavbars]: [Navbar[], any] = useState([]) const [series, setSeries]: [Story[], any] = useState([]) const [currentNavbar, setCurrentNavbar]: [Navbar, any] = useState(null) const { isOpen, onOpen, onClose } = useDisclosure() @@ -24,12 +39,18 @@ const UserNavbarPage = () => { }, []) const getNavbars = async () => { - const res = await requestApi.get("/user/navbars/0") + const res = await requestApi.get(`/user/navbars/${orgID ? orgID : 0}`) setNavbars(res.data) } const getSeries = async () => { - const res = await requestApi.get(`/story/posts/editor?type=${IDType.Series}`) + let res + if (orgID) { + res = await requestApi.get(`/story/posts/org/${orgID}?type=${IDType.Series}`) + } else { + res = await requestApi.get(`/story/posts/editor?type=${IDType.Series}`) + } + setSeries(res.data) } @@ -41,7 +62,7 @@ const UserNavbarPage = () => { duration: 2000, isClosable: true, }) - return + return } if (currentNavbar.label.length > config.user.navbarMaxLen) { @@ -51,11 +72,15 @@ const UserNavbarPage = () => { duration: 2000, isClosable: true, }) - return + return } + if (orgID) { + await requestApi.post(`/org/navbar/${orgID}`,currentNavbar) + } else { + await requestApi.post(`/user/navbar`, currentNavbar) + } - await requestApi.post(`/user/navbar`, currentNavbar) setCurrentNavbar(null) onClose() getNavbars() @@ -77,7 +102,7 @@ const UserNavbarPage = () => { } const onNvTypeChange = v => { - const tp = parseInt(v); + const tp = parseInt(v); currentNavbar.type = tp if (tp === NavbarType.Link) { currentNavbar.value = "" @@ -97,50 +122,44 @@ const UserNavbarPage = () => { } const onDeleteNavbar = async id => { - requestApi.delete(`/user/navbar/${id}`) + await requestApi.delete(`/${orgID?'org':'user'}/navbar/${id}`) getNavbars() } return ( <> - - - - - - 菜单设置 - - - - - - - - - - - - - - { - navbars.map((nv,i) => - - - - - - ) - } - - -
LabelTypeValueWeight
{nv.label}{nv.type === NavbarType.Link ? "link" : "series"}{nv.type === NavbarType.Link ? nv.value : getSeriesTitle(nv.value)}{nv.weight} - onEditNavbar(nv)}/> - onDeleteNavbar(nv.id)} /> -
-
-
-
- + + + 菜单设置 + + + + + + + + + + + + + + { + navbars.map((nv, i) => + + + + + + ) + } + + +
LabelTypeValueWeight
{nv.label}{nv.type === NavbarType.Link ? "link" : "series"}{nv.type === NavbarType.Link ? nv.value : getSeriesTitle(nv.value)}{nv.weight} + onEditNavbar(nv)} /> + onDeleteNavbar(nv.id)} /> +
+
{currentNavbar && @@ -185,6 +204,4 @@ const UserNavbarPage = () => { ) -} -export default UserNavbarPage - +} \ No newline at end of file diff --git a/pages/settings/org/navbar/[org_id].tsx b/pages/settings/org/navbar/[org_id].tsx new file mode 100644 index 00000000..e9715aaf --- /dev/null +++ b/pages/settings/org/navbar/[org_id].tsx @@ -0,0 +1,35 @@ +import { Box } from "@chakra-ui/react" +import PageContainer from "layouts/page-container" +import PageContainer1 from "layouts/page-container1" +import Sidebar from "layouts/sidebar/sidebar" +import { useRouter } from "next/router" +import { NavbarEditor } from "pages/settings/navbar" +import { useEffect, useState } from "react" +import { orgSettingLinks } from "src/data/links" +import { User } from "src/types/user" +import { requestApi } from "utils/axios/request" + +const UserNavbarPage = () => { + const [org, setOrg]:[User,any] = useState(null) + const router = useRouter() + const orgID = router.query.org_id + useEffect(() => { + if (orgID) { + requestApi.get(`/user/info/${router.query.org_id}`).then(res => setOrg(res.data)) + } + + }, [orgID]) + + + return ( + <> + + + + {orgID && } + + + + ) +} +export default UserNavbarPage \ No newline at end of file diff --git a/pages/settings/org/series/[org_id].tsx b/pages/settings/org/series/[org_id].tsx new file mode 100644 index 00000000..6eadabf4 --- /dev/null +++ b/pages/settings/org/series/[org_id].tsx @@ -0,0 +1,48 @@ +import { Text, Box, VStack, Divider, useToast, Heading, Alert, Tag, Button, HStack, Modal, ModalOverlay, ModalContent, ModalBody, Select, useDisclosure, Flex } from "@chakra-ui/react" +import Card from "components/card" +import Nav from "layouts/nav/nav" +import PageContainer from "layouts/page-container" +import Sidebar from "layouts/sidebar/sidebar" +import React, { useEffect, useState } from "react" +import { orgSettingLinks } from "src/data/links" +import { requestApi } from "utils/axios/request" +import { useRouter } from "next/router" +import { User } from "src/types/user" +import UserCard from "components/users/user-card" +import { config } from "configs/config" +import OrgMember from "components/users/org-member" +import { Role } from "src/types/role" +import { cloneDeep } from "lodash" +import { Story } from "src/types/story" +import Empty from "components/empty" +import ManageStories from "components/story/manage-stories" +import { IDType } from "src/types/id" +import { SeriesEditor } from "pages/editor/series" + + +const OrgPostsPage = () => { + const [org, setOrg]:[User,any] = useState(null) + const router = useRouter() + const orgID = router.query.org_id + useEffect(() => { + if (orgID) { + requestApi.get(`/user/info/${router.query.org_id}`).then(res => setOrg(res.data)) + } + + }, [orgID]) + + + + return ( + <> + + + + {orgID && } + + + + ) +} +export default OrgPostsPage + diff --git a/server/internal/api/api.go b/server/internal/api/api.go index 4ec29f76..ba8045cc 100644 --- a/server/internal/api/api.go +++ b/server/internal/api/api.go @@ -1,7 +1,34 @@ package api -import "github.com/imdotdev/im.dev/server/pkg/log" +import ( + "github.com/imdotdev/im.dev/server/internal/org" + "github.com/imdotdev/im.dev/server/internal/story" + "github.com/imdotdev/im.dev/server/pkg/log" + "github.com/imdotdev/im.dev/server/pkg/models" +) var logger = log.RootLogger.New("logger", "api") /* 鉴权、数据合法性验证都在api模块进行处理 */ + +func isStoryCreator(userID string, storyID string) bool { + if models.GetIDType(storyID) == models.IDTypeSeries { + // 如果是series,需要判断它属于组织还是个人,两者的权限验证不同 + story, _ := story.GetStory(storyID, "") + if story.OwnerID != "" { + if !org.IsOrgAdmin(userID, story.OwnerID) { + return false + } + } else { + if !models.IsStoryCreator(userID, storyID) { + return false + } + } + } else { + if !models.IsStoryCreator(userID, storyID) { + return false + } + } + + return true +} diff --git a/server/internal/api/org.go b/server/internal/api/org.go index b1db00bf..a7995209 100644 --- a/server/internal/api/org.go +++ b/server/internal/api/org.go @@ -372,3 +372,53 @@ func PinOrgStory(c *gin.Context) { c.JSON(http.StatusOK, common.RespSuccess(nil)) } + +func SubmitOrgNavbar(c *gin.Context) { + orgID := c.Param("orgID") + u := user.CurrentUser(c) + if !org.IsOrgAdmin(u.ID, orgID) { + c.JSON(http.StatusForbidden, common.RespError(e.NoAdminPermission)) + return + } + + nav := &models.Navbar{} + err := c.Bind(&nav) + if err != nil || !models.ValidNavbarType(nav.Type) { + c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid)) + return + } + + nav.UserID = orgID + err1 := user.SubmitNavbar(nav) + if err != nil { + c.JSON(err1.Status, common.RespError(err1.Message)) + return + } + + c.JSON(http.StatusOK, common.RespSuccess(nil)) +} + +func DeleteOrgNavbar(c *gin.Context) { + id := c.Param("id") + + u := user.CurrentUser(c) + + nav, err := user.GetNavbar(id) + if err != nil { + c.JSON(err.Status, common.RespError(err.Message)) + return + } + + if !org.IsOrgAdmin(u.ID, nav.UserID) { + c.JSON(http.StatusForbidden, common.RespError(e.NoAdminPermission)) + return + } + + err = user.DeleteNavbar(nav.UserID, id) + if err != nil { + c.JSON(err.Status, common.RespError(err.Message)) + return + } + + c.JSON(http.StatusOK, common.RespSuccess(nil)) +} diff --git a/server/internal/api/story.go b/server/internal/api/story.go index 8644c246..1753d85f 100644 --- a/server/internal/api/story.go +++ b/server/internal/api/story.go @@ -41,7 +41,7 @@ func DeletePost(c *gin.Context) { } u := user.CurrentUser(c) - if !models.IsStoryCreator(u.ID, id) { + if !isStoryCreator(u.ID, id) { c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) return } @@ -105,10 +105,11 @@ func SubmitSeriesPost(c *gin.Context) { } u := user.CurrentUser(c) - if !models.IsStoryCreator(u.ID, seriesID) { + if !isStoryCreator(u.ID, seriesID) { c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) return } + posts := make([]*models.SeriesPost, 0) err0 := c.Bind(&posts) if err0 != nil { @@ -157,11 +158,10 @@ func DeleteSeriesPost(c *gin.Context) { } u := user.CurrentUser(c) - if !models.IsStoryCreator(u.ID, id) { + if !isStoryCreator(u.ID, id) { c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) return } - err := story.DeleteSeriesPost(id) if err != nil { c.JSON(err.Status, common.RespError(err.Message)) @@ -207,8 +207,9 @@ func PinStory(c *gin.Context) { storyID := c.Param("storyID") u := user.CurrentUser(c) - if !models.IsStoryCreator(u.ID, storyID) { + if !isStoryCreator(u.ID, storyID) { c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) + return } err := story.PinStory(storyID, u.ID) diff --git a/server/internal/server.go b/server/internal/server.go index 796f73c8..689231fd 100644 --- a/server/internal/server.go +++ b/server/internal/server.go @@ -117,6 +117,9 @@ func (s *Server) Start() error { r.POST("/org/leave/:orgID", IsLogin(), api.LeaveOrg) r.DELETE("/org/post/:orgID/:postID", api.DeleteOrgPost) r.POST("/org/pin/story/:id", IsLogin(), api.PinOrgStory) + r.POST("/org/navbar/:orgID", IsLogin(), api.SubmitOrgNavbar) + r.DELETE("/org/navbar/:id", IsLogin(), api.DeleteOrgNavbar) + // admin apis r.POST("/admin/user", IsLogin(), api.AdminSubmitUser) r.GET("/admin/user/all", IsLogin(), api.AdminGetUsers) diff --git a/server/internal/story/post.go b/server/internal/story/post.go index 3e2fc7ac..72a198db 100644 --- a/server/internal/story/post.go +++ b/server/internal/story/post.go @@ -71,10 +71,19 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) { } } + post.CreatorID = user.ID // check user is in org exist if post.OwnerID != "" { - if !org.UserInOrg(user.ID, post.OwnerID) { - return nil, e.New(http.StatusForbidden, e.NoEditorPermission) + if models.GetIDType(post.ID) == models.IDTypeSeries { + // 组织的series所有权和创作权都归组织所有 + post.CreatorID = post.OwnerID + if !org.IsOrgAdmin(user.ID, post.OwnerID) { + return nil, e.New(http.StatusForbidden, e.NoAdminPermission) + } + } else { + if !org.UserInOrg(user.ID, post.OwnerID) { + return nil, e.New(http.StatusForbidden, e.NoEditorPermission) + } } } @@ -92,7 +101,7 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) { //create _, err := db.Conn.Exec("INSERT INTO story (id,type,creator,owner,slug, title, md, url, cover, brief,status, created, updated) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)", - post.ID, post.Type, user.ID, post.OwnerID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, models.StatusPublished, now, now) + post.ID, post.Type, post.CreatorID, post.OwnerID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, models.StatusPublished, now, now) if err != nil { logger.Warn("submit post error", "error", err) return nil, e.New(http.StatusInternalServerError, e.Internal) @@ -100,7 +109,7 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) { } else { // 只有创建者自己才能更新内容 creator, _ := GetPostCreator(post.ID) - if creator != user.ID { + if creator != post.CreatorID { return nil, e.New(http.StatusForbidden, e.NoEditorPermission) } diff --git a/server/internal/user/navbar.go b/server/internal/user/navbar.go index 4436e666..d3959126 100644 --- a/server/internal/user/navbar.go +++ b/server/internal/user/navbar.go @@ -1,6 +1,7 @@ package user import ( + "database/sql" "net/http" "sort" @@ -54,3 +55,19 @@ func DeleteNavbar(userID string, id string) *e.Error { return nil } + +func GetNavbar(id string) (*models.Navbar, *e.Error) { + nav := &models.Navbar{} + err := db.Conn.QueryRow("SELECT user_id,label,type,value,weight FROM user_navbar WHERE id=?", id).Scan( + &nav.UserID, &nav.Label, &nav.Type, &nav.Value, &nav.Weight, + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, e.New(http.StatusNotFound, e.NotFound) + } + logger.Warn("select navbar error", "error", err) + return nil, e.New(http.StatusInternalServerError, e.Internal) + } + + return nav, nil +} diff --git a/src/components/story/manage-stories.tsx b/src/components/story/manage-stories.tsx index 69ad3733..fe30eaa2 100644 --- a/src/components/story/manage-stories.tsx +++ b/src/components/story/manage-stories.tsx @@ -38,7 +38,7 @@ export const ManageStories = (props: Props) => { )} - {showFooter &&
没有更多文章了
} + {showFooter &&
没有更多文章了
} ) } diff --git a/src/data/links.tsx b/src/data/links.tsx index 2780d15a..1a93545e 100644 --- a/src/data/links.tsx +++ b/src/data/links.tsx @@ -119,6 +119,18 @@ export function orgSettingLinks(orgID) { icon: getSvgIcon("post"), disabled: false }, + { + title: '系列管理', + path: `${ReserveUrls.Settings}/org/series/${orgID}`, + icon: getSvgIcon("series"), + disabled: false + }, + { + title: '菜单管理', + path: `${ReserveUrls.Settings}/org/navbar/${orgID}`, + icon: getSvgIcon("navbar"), + disabled: false + }, ] }