pull/52/head
sunface 4 years ago
parent 00797230ad
commit 0d8816f305

@ -25,6 +25,21 @@ var validator = require('validator');
const newSeries: Story = { title: '', brief: '', cover: '', type: IDType.Series }
const SeriesPage = () => {
return (
<>
<PageContainer1 >
<Box display="flex">
<Sidebar routes={editorLinks} title="创作中心" />
<SeriesEditor />
</Box>
</PageContainer1>
</>
)
}
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 (
<>
<PageContainer1 >
<Box display="flex">
<Sidebar routes={editorLinks} title="创作中心" />
<Card ml="4" p="6" width="100%">
{currentSeries ?
<>
@ -293,10 +327,5 @@ const SeriesPage = () => {
}
</>}
</Card>
</Box>
</PageContainer1>
</>
)
}
export default SeriesPage
}

@ -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 (
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={settingLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="博客设置" />
<NavbarEditor />
</Box>
</PageContainer>
</>
)
}
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 (
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={settingLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="博客设置" />
<Card ml="4" width="100%">
<Flex justifyContent="space-between" alignItems="center">
<Heading size="sm"></Heading>
<Button colorScheme="teal" size="sm" onClick={onAddNavbar} _focus={null}></Button>
</Flex>
<Table variant="simple" mt="4">
<Thead>
<Tr>
<Th>Label</Th>
<Th>Type</Th>
<Th>Value</Th>
<Th>Weight</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{
navbars.map((nv,i) => <Tr key={i}>
<Td>{nv.label}</Td>
<Td>{nv.type === NavbarType.Link ? "link" : "series"}</Td>
<Td>{nv.type === NavbarType.Link ? nv.value : getSeriesTitle(nv.value)}</Td>
<Td>{nv.weight}</Td>
<Td>
<IconButton aria-label="edit navbar" variant="ghost" icon={getSvgIcon('edit', ".95rem")} onClick={() => onEditNavbar(nv)}/>
<IconButton aria-label="delete navbar" variant="ghost" icon={getSvgIcon('close', "1rem")} onClick={() => onDeleteNavbar(nv.id)} />
</Td>
</Tr>)
}
</Tbody>
</Table>
</Card>
</Box>
</PageContainer>
<Card ml="4" width="100%">
<Flex justifyContent="space-between" alignItems="center">
<Heading size="sm"></Heading>
<Button colorScheme="teal" size="sm" onClick={onAddNavbar} _focus={null}></Button>
</Flex>
<Table variant="simple" mt="4">
<Thead>
<Tr>
<Th>Label</Th>
<Th>Type</Th>
<Th>Value</Th>
<Th>Weight</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{
navbars.map((nv, i) => <Tr key={i}>
<Td>{nv.label}</Td>
<Td>{nv.type === NavbarType.Link ? "link" : "series"}</Td>
<Td>{nv.type === NavbarType.Link ? nv.value : getSeriesTitle(nv.value)}</Td>
<Td>{nv.weight}</Td>
<Td>
<IconButton aria-label="edit navbar" variant="ghost" icon={getSvgIcon('edit', ".95rem")} onClick={() => onEditNavbar(nv)} />
<IconButton aria-label="delete navbar" variant="ghost" icon={getSvgIcon('close', "1rem")} onClick={() => onDeleteNavbar(nv.id)} />
</Td>
</Tr>)
}
</Tbody>
</Table>
</Card>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
{currentNavbar && <ModalContent>
@ -185,6 +204,4 @@ const UserNavbarPage = () => {
</Modal>
</>
)
}
export default UserNavbarPage
}

@ -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 (
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={orgSettingLinks(router.query.org_id)} width={["120px", "120px", "250px", "250px"]} height="fit-content" title={`组织${org?.nickname}`} />
{orgID && <NavbarEditor orgID={orgID as string}/>}
</Box>
</PageContainer>
</>
)
}
export default UserNavbarPage

@ -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 (
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={orgSettingLinks(router.query.org_id)} width={["120px", "120px", "250px", "250px"]} height="fit-content" title={`组织${org?.nickname}`} />
{orgID && <SeriesEditor orgID={orgID as string}/>}
</Box>
</PageContainer>
</>
)
}
export default OrgPostsPage

@ -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
}

@ -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))
}

@ -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)

@ -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)

@ -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)
}

@ -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
}

@ -38,7 +38,7 @@ export const ManageStories = (props: Props) => {
<ManageStoryCard showSource={showSource} story={story} highlight={props.highlight} {...rest}/>
</Box>)}
</VStack>
{showFooter && <Center><Text layerStyle="textSecondary" fontSize="sm" pt="4"></Text></Center>}
{showFooter && <Center><Text layerStyle="textSecondary" fontSize="sm" pt="5"></Text></Center>}
</>
)
}

@ -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
},
]
}

Loading…
Cancel
Save