pull/50/head
sunface 4 years ago
parent 3a1c406da4
commit 453d8c7718

@ -26,6 +26,7 @@ paths:
#################################### Posts ############################## #################################### Posts ##############################
posts: posts:
brief_max_len: 100 title_max_len: 128
brief_max_len: 128
# whether allow writing posts # whether allow writing posts
writing_enabled: true writing_enabled: true

@ -8,6 +8,7 @@ require (
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/go-stack/stack v1.8.0 github.com/go-stack/stack v1.8.0
github.com/golang/snappy v0.0.2 github.com/golang/snappy v0.0.2
github.com/gosimple/slug v1.9.0
github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac
github.com/karrick/godirwalk v1.16.1 // indirect github.com/karrick/godirwalk v1.16.1 // indirect
github.com/keegancsmith/rpc v1.3.0 // indirect github.com/keegancsmith/rpc v1.3.0 // indirect

@ -88,6 +88,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs=
github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
@ -180,6 +182,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
github.com/ramya-rao-a/go-outline v0.0.0-20200117021646-2a048b4510eb h1:ilZSL4VaIq4Hsi+lH928xQKnSWymFug6r2gJomUBpW8= github.com/ramya-rao-a/go-outline v0.0.0-20200117021646-2a048b4510eb h1:ilZSL4VaIq4Hsi+lH928xQKnSWymFug6r2gJomUBpW8=
github.com/ramya-rao-a/go-outline v0.0.0-20200117021646-2a048b4510eb/go.mod h1:1WL5IqM+CnRCAbXetRnL1YVoS9KtU2zMhOi/5oAVPo4= github.com/ramya-rao-a/go-outline v0.0.0-20200117021646-2a048b4510eb/go.mod h1:1WL5IqM+CnRCAbXetRnL1YVoS9KtU2zMhOi/5oAVPo4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=

@ -8,7 +8,14 @@ import {
Box, Box,
useRadioGroup, useRadioGroup,
HStack, HStack,
Input Input,
Drawer,
useDisclosure,
DrawerOverlay,
DrawerContent,
Text,
Divider,
Heading
} from "@chakra-ui/react" } from "@chakra-ui/react"
import { useViewportScroll } from "framer-motion" import { useViewportScroll } from "framer-motion"
import NextLink from "next/link" import NextLink from "next/link"
@ -17,6 +24,7 @@ import { FaMoon, FaSun } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo" import Logo, { LogoIcon } from "src/components/logo"
import RadioCard from "components/radio-card" import RadioCard from "components/radio-card"
import { EditMode } from "src/types/editor" import { EditMode } from "src/types/editor"
import Card from "components/card"
@ -25,6 +33,7 @@ function HeaderContent(props:any) {
const { toggleColorMode: toggleMode } = useColorMode() const { toggleColorMode: toggleMode } = useColorMode()
const text = useColorModeValue("dark", "light") const text = useColorModeValue("dark", "light")
const SwitchIcon = useColorModeValue(FaMoon, FaSun) const SwitchIcon = useColorModeValue(FaMoon, FaSun)
const { isOpen, onOpen, onClose } = useDisclosure()
const editOptions = [EditMode.Edit, EditMode.Preview] const editOptions = [EditMode.Edit, EditMode.Preview]
const { getRootProps, getRadioProps } = useRadioGroup({ const { getRootProps, getRadioProps } = useRadioGroup({
@ -79,9 +88,33 @@ function HeaderContent(props:any) {
_focus={null} _focus={null}
icon={<SwitchIcon />} icon={<SwitchIcon />}
/> />
<Button layerStyle="colorButton" ml="2" onClick={props.publish}></Button> <Button layerStyle="colorButton" ml="2" onClick={onOpen}></Button>
</Box> </Box>
</Flex> </Flex>
<Drawer
isOpen={isOpen}
placement="right"
onClose={onClose}
size="md"
motionPreset="none"
>
<DrawerOverlay>
<DrawerContent p="4">
<Flex justifyContent="space-between" alignItems="center">
<Heading size="sm"></Heading>
<Button layerStyle="colorButton" ml="2" onClick={props.publish}></Button>
</Flex>
<Divider mt="5" mb="5"/>
<Card>
<Heading size="xs">
</Heading>
<Input value={props.ar.cover} onChange={(e) => {props.ar.cover = e.target.value; props.onChange()}} mt="4" variant="flushed" size="sm" placeholder="图片链接你可以用github当图片存储服务" focusBorderColor="teal.400"/>
</Card>
</DrawerContent>
</DrawerOverlay>
</Drawer>
</> </>
) )
} }

@ -1,4 +1,4 @@
import { Box, Button,createStandaloneToast} from '@chakra-ui/react'; import { Box, Button, useToast} from '@chakra-ui/react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { MarkdownEditor } from 'components/markdown-editor/editor'; import { MarkdownEditor } from 'components/markdown-editor/editor';
import PageContainer from 'layouts/page-container'; import PageContainer from 'layouts/page-container';
@ -8,7 +8,8 @@ import { MarkdownRender } from 'components/markdown-editor/render';
import { Post } from 'src/types/posts'; import { Post } from 'src/types/posts';
import { requestApi } from 'utils/axios/request'; import { requestApi } from 'utils/axios/request';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
const toast = createStandaloneToast() import { config } from 'utils/config';
import { cloneDeep } from 'lodash';
const content = ` const content = `
# test # test
@ -23,36 +24,56 @@ function PostEditPage() {
title: '' title: ''
}) })
const toast = useToast()
useEffect(() => { useEffect(() => {
if (id && id !== 'new') { if (id && id !== 'new') {
requestApi.get(`/editor/post/${id}`).then(res => setAr(res.data)) requestApi.get(`/editor/post/${id}`).then(res => setAr(res.data))
} }
},[id]) },[id])
const onChange = newMd => {
const onMdChange = newMd => {
setAr({ setAr({
...ar, ...ar,
md: newMd md: newMd
}) })
} }
const onChange = () => {
setAr(cloneDeep(ar))
}
const onChangeTitle = title => {
if (title.length > config.posts.titleMaxLen) {
toast({
description: `Title长度不能超过${config.posts.titleMaxLen}`,
status: "error",
duration: 2000,
isClosable: true,
})
return
}
setAr({...ar, title: title})
}
const publish = async () => { const publish = async () => {
await requestApi.post(`/editor/post`, ar) const res = await requestApi.post(`/editor/post`, ar)
toast({ toast({
description: "发布成功", description: "发布成功",
status: "success", status: "success",
duration: 2000, duration: 2000,
isClosable: true, isClosable: true,
}) })
router.push('/editor/posts') router.push(`/${res.data.username}/${res.data.slug}`)
} }
console.log(ar)
return ( return (
<PageContainer <PageContainer
nav={<EditorNav nav={<EditorNav
ar={ar} ar={ar}
onChange={onChange}
changeEditMode={(v) => setEditMode(v)} changeEditMode={(v) => setEditMode(v)}
changeTitle={(e) => {setAr({...ar, title: e.target.value})}} changeTitle={(e) => onChangeTitle(e.target.value)}
publish={() => publish()} publish={() => publish()}
/>} />}
> >
@ -66,7 +87,7 @@ function PostEditPage() {
}, },
}, },
}} }}
onChange={(md) => onChange(md)} onChange={(md) => onMdChange(md)}
md={ar.md} md={ar.md}
/> : /> :
<Box height="100%" p="6"> <Box height="100%" p="6">

@ -1,4 +1,4 @@
import { Menu,MenuButton,MenuList,MenuItem,createStandaloneToast, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue } from "@chakra-ui/react" import { Menu,MenuButton,MenuList,MenuItem, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue, useToast } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Nav from "layouts/nav/nav" import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container" import PageContainer from "layouts/page-container"
@ -14,7 +14,6 @@ import { Post } from "src/types/posts"
import { FaExternalLinkAlt, FaRegEdit } from "react-icons/fa" import { FaExternalLinkAlt, FaRegEdit } from "react-icons/fa"
import { useRouter } from "next/router" import { useRouter } from "next/router"
var validator = require('validator'); var validator = require('validator');
const toast = createStandaloneToast()
const newPost: Post = { title: '', url: '', cover: '' } const newPost: Post = { title: '', url: '', cover: '' }
const PostsPage = () => { const PostsPage = () => {
@ -22,6 +21,7 @@ const PostsPage = () => {
const [posts, setPosts] = useState([]) const [posts, setPosts] = useState([])
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
const router = useRouter() const router = useRouter()
const toast = useToast()
const getPosts = () => { const getPosts = () => {
requestApi.get(`/editor/posts`).then((res) => setPosts(res.data)).catch(_ => setPosts([])) requestApi.get(`/editor/posts`).then((res) => setPosts(res.data)).catch(_ => setPosts([]))
} }
@ -32,11 +32,15 @@ const PostsPage = () => {
function validateTitle(value) { function validateTitle(value) {
console.log(value)
let error let error
if (!value?.trim()) { if (!value?.trim()) {
error = "标题不能为空" error = "标题不能为空"
} }
if (value?.length > config.posts.titleMaxLen) {
error = "标题长度不能超过128"
}
return error return error
} }

@ -24,14 +24,13 @@ func GetEditorPosts(c *gin.Context) {
} }
func SubmitPost(c *gin.Context) { func SubmitPost(c *gin.Context) {
err := posts.SubmitPost(c) res, err := posts.SubmitPost(c)
if err != nil { if err != nil {
logger.Warn("submit post error", "error", err)
c.JSON(err.Status, common.RespError(err.Message)) c.JSON(err.Status, common.RespError(err.Message))
return return
} }
c.JSON(http.StatusOK, common.RespSuccess(nil)) c.JSON(http.StatusOK, common.RespSuccess(res))
} }
func DeletePost(c *gin.Context) { func DeletePost(c *gin.Context) {

@ -1,141 +0,0 @@
package posts
import (
"database/sql"
"net/http"
"sort"
"strings"
"time"
"unicode/utf8"
"github.com/asaskevich/govalidator"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/pkg/config"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
"github.com/imdotdev/im.dev/server/pkg/utils"
)
func UserPosts(uid int64) (models.Posts, *e.Error) {
ars := make(models.Posts, 0)
rows, err := db.Conn.Query("select id,title,url,cover,brief,created,updated from posts where creator=?", uid)
if err != nil {
if err == sql.ErrNoRows {
return ars, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal)
}
creator := &models.UserSimple{ID: uid}
creator.Query()
for rows.Next() {
ar := &models.Post{}
err := rows.Scan(&ar.ID, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Created, &ar.Updated)
if err != nil {
logger.Warn("scan post error", "error", err)
continue
}
ar.Creator = creator
ars = append(ars, ar)
}
sort.Sort(ars)
return ars, nil
}
func SubmitPost(c *gin.Context) *e.Error {
user := session.CurrentUser(c)
if !user.Role.IsEditor() {
return e.New(http.StatusForbidden, e.NoEditorPermission)
}
ar := &models.Post{}
err := c.Bind(&ar)
if err != nil {
return e.New(http.StatusBadRequest, e.ParamInvalid)
}
if strings.TrimSpace(ar.Title) == "" || utf8.RuneCountInString(ar.Brief) > config.Data.Posts.BriefMaxLen {
return e.New(http.StatusBadRequest, e.ParamInvalid)
}
if strings.TrimSpace(ar.URL) != "" && !govalidator.IsURL(ar.URL) {
return e.New(http.StatusBadRequest, e.ParamInvalid)
}
if strings.TrimSpace(ar.Cover) != "" && !govalidator.IsURL(ar.Cover) {
return e.New(http.StatusBadRequest, e.ParamInvalid)
}
now := time.Now()
md := utils.Compress(ar.Md)
if ar.ID == 0 {
//create
_, err = db.Conn.Exec("INSERT INTO posts (creator, title, md, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?,?)",
user.ID, ar.Title, md, ar.URL, ar.Cover, ar.Brief, now, now)
if err != nil {
logger.Warn("submit post error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
} else {
_, err = db.Conn.Exec("UPDATE posts SET title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?",
ar.Title, md, ar.URL, ar.Cover, ar.Brief, now, ar.ID)
if err != nil {
logger.Warn("upate post error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
}
return nil
}
func DeletePost(id int64) *e.Error {
_, err := db.Conn.Exec("DELETE FROM posts WHERE id=?", id)
if err != nil {
logger.Warn("delete post error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}
func GetPost(id int64) (*models.Post, *e.Error) {
ar := &models.Post{}
var rawmd []byte
err := db.Conn.QueryRow("select id,title,md,url,cover,brief,creator,created,updated from posts where id=?", id).Scan(
&ar.ID, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("get post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
md, _ := utils.Uncompress(rawmd)
ar.Md = string(md)
ar.Creator = &models.UserSimple{ID: ar.CreatorID}
err = ar.Creator.Query()
return ar, nil
}
func GetPostCreator(id int64) (int64, *e.Error) {
var uid int64
err := db.Conn.QueryRow("SELECT creator FROM posts WHERE id=?", id).Scan(&uid)
if err != nil {
if err == sql.ErrNoRows {
return 0, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("get post creator error", "error", err)
return 0, e.New(http.StatusInternalServerError, e.Internal)
}
return uid, nil
}

@ -0,0 +1,200 @@
package posts
import (
"database/sql"
"fmt"
"net/http"
"sort"
"strings"
"time"
"unicode/utf8"
"github.com/asaskevich/govalidator"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/pkg/config"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
"github.com/imdotdev/im.dev/server/pkg/utils"
)
func UserPosts(uid int64) (models.Posts, *e.Error) {
ars := make(models.Posts, 0)
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,created,updated from posts where creator=?", uid)
if err != nil {
if err == sql.ErrNoRows {
return ars, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal)
}
creator := &models.UserSimple{ID: uid}
creator.Query()
for rows.Next() {
ar := &models.Post{}
err := rows.Scan(&ar.ID, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Created, &ar.Updated)
if err != nil {
logger.Warn("scan post error", "error", err)
continue
}
ar.Creator = creator
ars = append(ars, ar)
}
sort.Sort(ars)
return ars, nil
}
func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
user := session.CurrentUser(c)
post := &models.Post{}
err := c.Bind(&post)
if err != nil {
return nil, e.New(http.StatusBadRequest, e.ParamInvalid)
}
if strings.TrimSpace(post.Title) == "" || utf8.RuneCountInString(post.Brief) > config.Data.Posts.BriefMaxLen {
return nil, e.New(http.StatusBadRequest, "标题格式不合法")
}
if strings.TrimSpace(post.URL) != "" && !govalidator.IsURL(post.URL) {
return nil, e.New(http.StatusBadRequest, "URL格式不正确")
}
if strings.TrimSpace(post.Cover) != "" && !govalidator.IsURL(post.Cover) {
return nil, e.New(http.StatusBadRequest, "图片链接格式不正确")
}
isExternal := true
if strings.TrimSpace(post.URL) == "" {
isExternal = false
}
if isExternal {
// internal post, need creator role
if !user.Role.IsCreator() {
return nil, e.New(http.StatusForbidden, e.NoEditorPermission)
}
} else {
// external post, need editor role
if !user.Role.IsEditor() {
return nil, e.New(http.StatusForbidden, e.NoEditorPermission)
}
if len(post.Md) <= config.Data.Posts.BriefMaxLen {
post.Brief = post.Md
} else {
post.Brief = string([]rune(post.Md)[:config.Data.Posts.BriefMaxLen])
}
}
now := time.Now()
md := utils.Compress(post.Md)
setSlug(user.ID, post)
if post.ID == 0 {
//create
_, err = db.Conn.Exec("INSERT INTO posts (creator,slug, title, md, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?,?,?)",
user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, now)
if err != nil {
logger.Warn("submit post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
} else {
// 只有创建者自己才能更新内容
creator, _ := GetPostCreator(post.ID)
if creator != user.ID {
return nil, e.New(http.StatusForbidden, e.NoEditorPermission)
}
_, err = db.Conn.Exec("UPDATE posts SET slug=?, title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?",
post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, post.ID)
if err != nil {
logger.Warn("upate post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
}
return map[string]string{
"username": user.Username,
"slug": post.Slug,
}, nil
}
func DeletePost(id int64) *e.Error {
_, err := db.Conn.Exec("DELETE FROM posts WHERE id=?", id)
if err != nil {
logger.Warn("delete post error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}
func GetPost(id int64) (*models.Post, *e.Error) {
ar := &models.Post{}
var rawmd []byte
err := db.Conn.QueryRow("select id,slug,title,md,url,cover,brief,creator,created,updated from posts where id=?", id).Scan(
&ar.ID, &ar.Slug, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("get post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
md, _ := utils.Uncompress(rawmd)
ar.Md = string(md)
ar.Creator = &models.UserSimple{ID: ar.CreatorID}
err = ar.Creator.Query()
return ar, nil
}
func GetPostCreator(id int64) (int64, *e.Error) {
var uid int64
err := db.Conn.QueryRow("SELECT creator FROM posts WHERE id=?", id).Scan(&uid)
if err != nil {
if err == sql.ErrNoRows {
return 0, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("get post creator error", "error", err)
return 0, e.New(http.StatusInternalServerError, e.Internal)
}
return uid, nil
}
//slug有三个规则
// 1. 长度不能超过127
// 2. 每次title更新都要重新生成slug
// 3. 单个用户下的slug不能重复如果已经存在需要加上-1这种字符
func setSlug(creator int64, post *models.Post) error {
slug := utils.Slugify(post.Title)
if len(slug) > 100 {
slug = slug[:100]
}
count := 0
err := db.Conn.QueryRow("SELECT count(*) FROM posts WHERE creator=? and title=?", creator, post.Title).Scan(&count)
if err != nil {
logger.Warn("count slug error", "error", err)
return err
}
fmt.Println(count)
if count == 0 {
post.Slug = slug
} else {
post.Slug = fmt.Sprintf("%s-%d", slug, count)
}
return nil
}

@ -31,7 +31,7 @@ var sqlTables = map[string]string{
"posts": `CREATE TABLE IF NOT EXISTS posts ( "posts": `CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
creator INTEGER NOT NULL, creator INTEGER NOT NULL,
slug VARCHAR(64) NOT NULL,
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
md TEXT, md TEXT,
url VARCHAR(255), url VARCHAR(255),
@ -45,5 +45,7 @@ var sqlTables = map[string]string{
ON posts (creator); ON posts (creator);
CREATE INDEX IF NOT EXISTS posts_created CREATE INDEX IF NOT EXISTS posts_created
ON posts (created); ON posts (created);
CREATE UNIQUE INDEX IF NOT EXISTS posts_creator_slug
ON posts (creator, slug);
`, `,
} }

@ -13,6 +13,7 @@ type UIConfig struct {
} }
type UIPosts struct { type UIPosts struct {
TitleMaxLen int `json:"titleMaxLen"`
BriefMaxLen int `json:"briefMaxLen"` BriefMaxLen int `json:"briefMaxLen"`
WritingEnabled bool `json:"writingEnabled"` WritingEnabled bool `json:"writingEnabled"`
} }
@ -20,6 +21,7 @@ type UIPosts struct {
func GetUIConfig(c *gin.Context) { func GetUIConfig(c *gin.Context) {
conf := &UIConfig{ conf := &UIConfig{
Posts: &UIPosts{ Posts: &UIPosts{
TitleMaxLen: config.Data.Posts.TitleMaxLen,
BriefMaxLen: config.Data.Posts.BriefMaxLen, BriefMaxLen: config.Data.Posts.BriefMaxLen,
WritingEnabled: config.Data.Posts.WritingEnabled, WritingEnabled: config.Data.Posts.WritingEnabled,
}, },

@ -33,6 +33,7 @@ type Config struct {
} }
Posts struct { Posts struct {
TitleMaxLen int `yaml:"title_max_len"`
BriefMaxLen int `yaml:"brief_max_len"` BriefMaxLen int `yaml:"brief_max_len"`
WritingEnabled bool `yaml:"writing_enabled"` WritingEnabled bool `yaml:"writing_enabled"`
} }

@ -7,6 +7,7 @@ type Post struct {
Creator *UserSimple `json:"creator"` Creator *UserSimple `json:"creator"`
CreatorID int64 `json:"creatorId"` CreatorID int64 `json:"creatorId"`
Title string `json:"title"` Title string `json:"title"`
Slug string `json:"slug"`
Md string `json:"md"` Md string `json:"md"`
URL string `json:"url"` URL string `json:"url"`
Cover string `json:"cover"` Cover string `json:"cover"`

@ -5,6 +5,7 @@ type RoleType string
const ( const (
ROLE_NORMAL = "Normal" ROLE_NORMAL = "Normal"
ROLE_EDITOR = "Editor" ROLE_EDITOR = "Editor"
ROLE_CREATOR = "Creator"
ROLE_ADMIN = "Admin" ROLE_ADMIN = "Admin"
ROLE_SUPER_ADMIN = "SuperAdmin" ROLE_SUPER_ADMIN = "SuperAdmin"
) )
@ -21,21 +22,6 @@ func (r RoleType) IsEditor() bool {
return r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_SUPER_ADMIN return r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_SUPER_ADMIN
} }
func IsAdmin(r RoleType) bool { func (r RoleType) IsCreator() bool {
return r.IsAdmin() return r == ROLE_CREATOR || r == ROLE_EDITOR || r == ROLE_SUPER_ADMIN
}
func RoleSortWeight(role RoleType) int {
switch role {
case ROLE_NORMAL:
return 0
case ROLE_EDITOR:
return 1
case ROLE_ADMIN:
return 2
case ROLE_SUPER_ADMIN:
return 3
default:
return 0
}
} }

@ -0,0 +1,23 @@
package utils
import (
"encoding/base64"
"strings"
"github.com/gosimple/slug"
)
func Slugify(raw string) string {
s := slug.Make(strings.ToLower(raw))
if s == "" {
// If the raw name is only characters outside of the
// sluggable characters, the slug creation will return an
// empty string which will mess up URLs. This failsafe picks
// that up and creates the slug as a base64 identifier instead.
s = base64.RawURLEncoding.EncodeToString([]byte(raw))
if slug.MaxLength != 0 && len(s) > slug.MaxLength {
s = s[:slug.MaxLength]
}
}
return s
}

@ -26,7 +26,7 @@ export const TextPostCard= (props:Props) =>{
</VStack> </VStack>
{props.showActions && <HStack> {props.showActions && <HStack>
<Button size="sm" colorScheme="teal" variant="outline" onClick={onEdit}>Edit</Button> <Button size="sm" colorScheme="teal" variant="outline" onClick={onEdit}>Edit</Button>
<Button size="sm" onClick={props.onDelete}>Delete</Button> <Button size="sm" onClick={props.onDelete} variant="ghost">Delete</Button>
</HStack>} </HStack>}
</Flex> </Flex>
) )

@ -1,6 +1,7 @@
export enum Role { export enum Role {
NORMAL = "Normal", NORMAL = "Normal",
EDITOR = "Editor", EDITOR = "Editor",
ROLE_CREATOR = "Creator",
ADMIN = "Admin", ADMIN = "Admin",
SUPER_ADMIN = "SuperAdmin" SUPER_ADMIN = "SuperAdmin"
} }

@ -2,7 +2,8 @@ import { requestApi } from "./axios/request"
export let config = { export let config = {
posts: { posts: {
briefMaxLen: 10, titleMaxLen: 128,
briefMaxLen: 128,
writingEnabled: false writingEnabled: false
} }
} }

@ -10,6 +10,6 @@ export default function layerStyles(theme) {
cursor: 'pointer' cursor: 'pointer'
}, },
_focus: null _focus: null
} },
} }
} }
Loading…
Cancel
Save