pull/52/head
sunface 4 years ago
parent 39b01f4cca
commit 5084bcb80e

@ -51,7 +51,7 @@ const UserPage = () => {
getTags(res.data.id) getTags(res.data.id)
getNavbars(res.data.id) getNavbars(res.data.id)
const res1 = await requestApi.get(`/user/posts/${res.data.id}`) const res1 = res.data.type === IDType.User ? await requestApi.get(`/user/posts/${res.data.id}`) : await requestApi.get(`/story/posts/org/${res.data.id}?type=0`)
setPosts(res1.data) setPosts(res1.data)
setRawPosts(res1.data) setRawPosts(res1.data)
} }
@ -251,7 +251,7 @@ const UserPage = () => {
</Card> </Card>
: :
<Card width="100%" height="fit-content" p="0"> <Card width="100%" height="fit-content" p="0">
<Stories stories={posts} showFooter={tagFilter === null} showPinned={true} /> <Stories stories={posts} showFooter={tagFilter === null} showPinned={true} showOrg={user.type === IDType.User}/>
</Card> </Card>
} }
</Box> </Box>

@ -0,0 +1,160 @@
import { Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast, Table, Thead, Tr, Th, Tbody, Td, IconButton, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, useDisclosure, FormControl, FormLabel, Input, FormErrorMessage, Select} from "@chakra-ui/react"
import Card from "components/card"
import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react"
import { adminLinks } from "src/data/links"
import { requestApi } from "utils/axios/request"
import TagCard from "components/tags/tag-card"
import { useRouter } from "next/router"
import Link from "next/link"
import { ReserveUrls } from "src/data/reserve-urls"
import { Tag } from "src/types/tag"
import { route } from "next/dist/next-server/server/router"
import PageContainer1 from "layouts/page-container1"
import Empty from "components/empty"
import { User } from "src/types/user"
import moment from 'moment'
import { getSvgIcon } from "components/svg-icon"
import { Field, Form, Formik } from "formik"
import { validateEmail, validateNickname, validateUsername } from "utils/user"
import { Role } from "src/types/role"
const PostsPage = () => {
const { isOpen, onOpen, onClose } = useDisclosure()
const [currentUser,setCurrentUser]:[User,any] = useState(null)
const [users, setUsers]: [User[], any] = useState([])
const router = useRouter()
const toast = useToast()
const getUsers = async () => {
const res = await requestApi.get(`/admin/user/all`)
setUsers(res.data)
}
useEffect(() => {
getUsers()
}, [])
const onEditUser = user => {
if (!user) {
// add user
setCurrentUser({role:Role.NORMAL})
} else {
// edit user
setCurrentUser(user)
}
onOpen()
}
const submitUser = async values => {
await requestApi.post("/admin/user",values)
getUsers()
onClose()
}
return (
<>
<PageContainer1>
<Box display="flex">
<Sidebar routes={adminLinks} title="管理员" />
<Card ml="4" p="6" width="100%">
<Flex alignItems="center" justify="space-between">
<Heading size="md">({users.length})</Heading>
<Button colorScheme="teal" size="sm" _focus={null} onClick={() => onEditUser(null)}></Button>
</Flex>
<Table variant="simple" mt="4">
<Thead>
<Tr>
<Th></Th>
<Th></Th>
<Th></Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{
users.map((user, i) => <Tr key={i}>
<Td>{user.username}</Td>
<Td>{user.email}</Td>
<Td>{user.role}</Td>
<Td>{moment(user.created).fromNow()}</Td>
<Td>
<IconButton aria-label="edit navbar" variant="ghost" icon={getSvgIcon('edit', ".95rem")} onClick={() => onEditUser(user)} />
{/* <IconButton aria-label="delete navbar" variant="ghost" icon={getSvgIcon('close', "1rem")} onClick={() => onDeleteUser(user)} /> */}
</Td>
</Tr>)
}
</Tbody>
</Table>
</Card>
</Box>
</PageContainer1>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
{currentUser && <ModalContent>
<ModalHeader>{currentUser.id ? '编辑用户' : '新建用户'}</ModalHeader>
<ModalBody mb="2">
<Formik
initialValues={currentUser}
onSubmit={submitUser}
>
{(props) => (
<Form>
<VStack>
<Field name="username" validate={currentUser.id !== undefined ? null : validateUsername}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.username && form.touched.username} >
<FormLabel>Username</FormLabel>
<Input {...field} placeholder="name" disabled={currentUser.id !== undefined}/>
<FormErrorMessage>{form.errors.username}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="email" validate={currentUser.id !== undefined ? null :validateEmail}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.email && form.touched.email} >
<FormLabel></FormLabel>
<Input {...field} placeholder="" size="lg" disabled={currentUser.id !== undefined}/>
<FormErrorMessage>{form.errors.email}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="role">
{({ field, form }) => (
<FormControl isInvalid={form.errors.role && form.touched.role} >
<FormLabel>Role</FormLabel>
<Select value={currentUser.role} {...field}>
<option value={Role.NORMAL}>{Role.NORMAL}</option>
<option value={Role.EDITOR}>{Role.EDITOR}</option>
<option value={Role.ADMIN}>{Role.ADMIN}</option>
</Select>
<FormErrorMessage>{form.errors.role}</FormErrorMessage>
</FormControl>
)}
</Field>
</VStack>
<Box mt={6}>
<Button
colorScheme="teal"
variant="outline"
type="submit"
_focus={null}
>
</Button>
<Button variant="ghost" ml="4" _focus={null} onClick={onClose}></Button>
</Box>
</Form>
)}
</Formik>
</ModalBody>
</ModalContent>}
</Modal>
</>
)
}
export default PostsPage

@ -1,4 +1,4 @@
import { Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, HStack, Wrap, useMediaQuery, Avatar, Textarea, Table, Thead, Tr, Th, Tbody, Td, IconButton, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, NumberInput, NumberInputField, NumberInputStepper, NumberIncrementStepper, NumberDecrementStepper, useColorModeValue, StackDivider } from "@chakra-ui/react" import { Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, HStack, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, useColorModeValue, StackDivider } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import PageContainer from "layouts/page-container" import PageContainer from "layouts/page-container"
import Sidebar from "layouts/sidebar/sidebar" import Sidebar from "layouts/sidebar/sidebar"
@ -8,7 +8,7 @@ import { requestApi } from "utils/axios/request"
import { Org } from "src/types/org" import { Org } from "src/types/org"
import { Field, Form, Formik } from "formik" import { Field, Form, Formik } from "formik"
import { config } from "configs/config" import { config } from "configs/config"
import { isUsernameChar, usernameInvalidTips } from "utils/user" import { isUsernameChar, usernameInvalidTips, validateNickname, validateUsername } from "utils/user"
import { isAdmin } from "utils/role" import { isAdmin } from "utils/role"
import userCustomTheme from "theme/user-custom" import userCustomTheme from "theme/user-custom"
import { useRouter } from "next/router" import { useRouter } from "next/router"
@ -41,41 +41,7 @@ const UserOrgsPage = () => {
onOpen() onOpen()
} }
const validateUsername = async value => {
let error
if (!value?.trim()) {
return "不能为空"
}
if (value?.length > config.user.usernameMaxLen) {
return `长度不能超过${config.user.usernameMaxLen}`
}
for (const c of value) {
if (!isUsernameChar(c)) {
return usernameInvalidTips
}
}
const res = await requestApi.get(`/username/exist/${value}`)
if (res.data) {
return `The name '${value}' is already taken.`
}
return error
}
function validateNickname(value) {
let error
if (!value?.trim()) {
error = "不能为空"
}
if (value?.length > config.user.usernameMaxLen) {
error = `长度不能超过${config.user.usernameMaxLen}`
}
return error
}
return ( return (

@ -10,7 +10,8 @@ import { useRouter } from "next/router"
import { Field, Form, Formik } from "formik" import { Field, Form, Formik } from "formik"
import { config } from "configs/config" import { config } from "configs/config"
import Tags from "components/tags/tags" import Tags from "components/tags/tags"
var validator = require('validator'); import { validateNickname ,validateEmail,validateUrl} from "utils/user"
const UserProfilePage = () => { const UserProfilePage = () => {
const [user, setUser] = useState(null) const [user, setUser] = useState(null)
@ -33,57 +34,8 @@ const UserProfilePage = () => {
}) })
} }
function validateNickname(value) {
let error
if (!value?.trim()) {
error = "昵称不能为空"
}
if (value?.length > config.user.nicknameMaxLen) {
error = `长度不能超过${config.user.nicknameMaxLen}`
}
return error
}
function validateEmail(value) {
let email = value?.trim()
let error
if (email?.length > config.user.usernameMaxLen) {
error = `长度不能超过${config.user.usernameMaxLen}`
return error
}
if (email) {
if (!validator.isEmail(email)) {
error = "Email格式不合法"
return error
}
}
return error
}
function validateUrl(value, canBeEmpty = true) {
let url = value?.trim()
let error
if (!canBeEmpty) {
if (!url) {
error = "url不能为空"
return error
}
}
if (url) {
if (!validator.isURL(value)) {
error = "URL格式不合法"
return error
}
}
return error
}
function validateLen(value) { function validateLen(value) {
let error let error

@ -0,0 +1,27 @@
package admin
import (
"net/http"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
)
func GetUsers() ([]*models.User, *e.Error) {
users := make([]*models.User, 0)
rows, err := db.Conn.Query("SELECT id,username,email,role,created FROM user WHERE type=?", models.IDTypeUser)
if err != nil {
logger.Warn("get users error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
for rows.Next() {
user := &models.User{}
rows.Scan(&user.ID, &user.Username, &user.Email, &user.Role, &user.Created)
users = append(users, user)
}
return users, nil
}

@ -0,0 +1,53 @@
package api
import (
"net/http"
"github.com/asaskevich/govalidator"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/admin"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
)
func AdminSubmitUser(c *gin.Context) {
currentUser := user.CurrentUser(c)
if !currentUser.Role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
u := &models.User{}
c.Bind(&u)
if u.Username == "" || u.Email == "" || !govalidator.IsEmail(u.Email) || !u.Role.IsValid() {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
err := user.SubmitUser(u)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func AdminGetUsers(c *gin.Context) {
currentUser := user.CurrentUser(c)
if !currentUser.Role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
users, err := admin.GetUsers()
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(users))
}

@ -27,6 +27,24 @@ func GetEditorPosts(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(ars)) c.JSON(http.StatusOK, common.RespSuccess(ars))
} }
func GetOrgPosts(c *gin.Context) {
orgID := c.Param("id")
tp := c.Query("type")
if tp != models.IDTypeUndefined && !models.ValidStoryIDType(tp) {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
user := user.CurrentUser(c)
ars, err := story.OrgPosts(tp, user, orgID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(ars))
}
func GetEditorDrafts(c *gin.Context) { func GetEditorDrafts(c *gin.Context) {
user := user.CurrentUser(c) user := user.CurrentUser(c)
ars, err := story.UserDrafts(nil, user.ID) ars, err := story.UserDrafts(nil, user.ID)

@ -143,7 +143,7 @@ func DeleteUserNavbar(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil)) c.JSON(http.StatusOK, common.RespSuccess(nil))
} }
func NameExist(c *gin.Context) { func UserNameExist(c *gin.Context) {
name := c.Param("name") name := c.Param("name")
if strings.TrimSpace(name) == "" { if strings.TrimSpace(name) == "" {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid)) c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
@ -158,3 +158,19 @@ func NameExist(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(exist)) c.JSON(http.StatusOK, common.RespSuccess(exist))
} }
func UserEmailExist(c *gin.Context) {
email := c.Param("email")
if strings.TrimSpace(email) == "" {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
exist, err := user.EmailExist(email)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(exist))
}

@ -16,7 +16,7 @@ func Init() {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
for { for {
// load users // load users
rows, err := db.Conn.Query(`SELECT id,username,role,nickname,avatar,last_seen_at,created FROM user`) rows, err := db.Conn.Query(`SELECT id,type,username,role,nickname,avatar,last_seen_at,created FROM user`)
if err != nil { if err != nil {
logger.Error("load users error", "error", err) logger.Error("load users error", "error", err)
time.Sleep(60 * time.Second) time.Sleep(60 * time.Second)
@ -27,7 +27,7 @@ func Init() {
usersMap := make(map[string]*models.User) usersMap := make(map[string]*models.User)
for rows.Next() { for rows.Next() {
user := &models.User{} user := &models.User{}
err := rows.Scan(&user.ID, &user.Username, &user.Role, &user.Nickname, &user.Avatar, &user.LastSeenAt, &user.Created) err := rows.Scan(&user.ID, &user.Type, &user.Username, &user.Role, &user.Nickname, &user.Avatar, &user.LastSeenAt, &user.Created)
if err != nil { if err != nil {
logger.Warn("scan user error", "error", err) logger.Warn("scan user error", "error", err)
continue continue

@ -135,7 +135,7 @@ func GetFollowers(targetID, targetType string) ([]*models.User, *e.Error) {
u, ok := models.UsersMapCache[id] u, ok := models.UsersMapCache[id]
if ok { if ok {
users = append(users, u) users = append(users, u)
u.Followed = GetFollowed(u.ID, targetID) u.Followed = GetFollowed(targetID, u.ID)
u.Follows = GetFollows(u.ID) u.Follows = GetFollows(u.ID)
} }
} }

@ -51,6 +51,7 @@ func (s *Server) Start() error {
r.POST("/story/comment", IsLogin(), api.SubmitComment) r.POST("/story/comment", IsLogin(), api.SubmitComment)
r.DELETE("/story/comment/:id", IsLogin(), api.DeleteStoryComment) r.DELETE("/story/comment/:id", IsLogin(), api.DeleteStoryComment)
r.GET("/story/posts/editor", IsLogin(), api.GetEditorPosts) r.GET("/story/posts/editor", IsLogin(), api.GetEditorPosts)
r.GET("/story/posts/org/:id", IsLogin(), api.GetOrgPosts)
r.GET("/story/posts/drafts", IsLogin(), api.GetEditorDrafts) r.GET("/story/posts/drafts", IsLogin(), api.GetEditorDrafts)
r.GET("/story/posts/home/:filter", api.GetHomePosts) r.GET("/story/posts/home/:filter", api.GetHomePosts)
r.POST("/story", IsLogin(), api.SubmitStory) r.POST("/story", IsLogin(), api.SubmitStory)
@ -87,6 +88,9 @@ func (s *Server) Start() error {
r.POST("/user/navbar", IsLogin(), api.SubmitUserNavbar) r.POST("/user/navbar", IsLogin(), api.SubmitUserNavbar)
r.GET("/user/navbars/:userID", api.GetUserNavbars) r.GET("/user/navbars/:userID", api.GetUserNavbars)
r.DELETE("/user/navbar/:id", IsLogin(), api.DeleteUserNavbar) r.DELETE("/user/navbar/:id", IsLogin(), api.DeleteUserNavbar)
r.GET("/user/name/exist/:name", api.UserNameExist)
r.GET("/user/email/exist/:email", api.UserEmailExist)
// interaction apis // interaction apis
r.POST("/interaction/like/:id", IsLogin(), api.Like) r.POST("/interaction/like/:id", IsLogin(), api.Like)
r.POST("/interaction/follow/:id", IsLogin(), api.Follow) r.POST("/interaction/follow/:id", IsLogin(), api.Follow)
@ -104,12 +108,17 @@ func (s *Server) Start() error {
r.POST("/org/update", IsLogin(), api.UpdateOrg) r.POST("/org/update", IsLogin(), api.UpdateOrg)
r.GET("/org/byUserID/:userID", api.GetOrgByUserID) r.GET("/org/byUserID/:userID", api.GetOrgByUserID)
r.GET("/org/members/:id", api.GetOrgMembers) r.GET("/org/members/:id", api.GetOrgMembers)
// admin apis
r.POST("/admin/user", IsLogin(), api.AdminSubmitUser)
r.GET("/admin/user/all", IsLogin(), api.AdminGetUsers)
// other apis // other apis
r.GET("/config", GetConfig) r.GET("/config", GetConfig)
r.GET("/navbars", GetNavbars) r.GET("/navbars", GetNavbars)
r.POST("/navbar", IsLogin(), SubmitNavbar) r.POST("/navbar", IsLogin(), SubmitNavbar)
r.DELETE("/navbar/:id", IsLogin(), DeleteNavbar) r.DELETE("/navbar/:id", IsLogin(), DeleteNavbar)
r.GET("/username/exist/:name", api.NameExist)
err := router.Run(config.Data.Server.Addr) err := router.Run(config.Data.Server.Addr)
if err != nil { if err != nil {
logger.Crit("start backend server error", "error", err) logger.Crit("start backend server error", "error", err)

@ -41,9 +41,9 @@ func Init() error {
} }
var navbars = []*models.Navbar{ var navbars = []*models.Navbar{
&models.Navbar{Label: "主页", Value: "/", Weight: 0}, &models.Navbar{Label: "主页", Value: "/", Weight: 2},
&models.Navbar{Label: "标签", Value: "/tags", Weight: 1}, &models.Navbar{Label: "标签", Value: "/tags", Weight: 1},
&models.Navbar{Label: "Search", Value: "/search/posts", Weight: 2}, &models.Navbar{Label: "Search", Value: "/search/posts", Weight: 0},
} }
func initTables() error { func initTables() error {

@ -64,6 +64,40 @@ func UserPosts(tp string, user *models.User, uid string) (models.Stories, *e.Err
return newPosts, nil return newPosts, nil
} }
func OrgPosts(tp string, user *models.User, orgID string) (models.Stories, *e.Error) {
var rows *sql.Rows
var err error
if tp == models.IDTypeUndefined {
rows, err = db.Conn.Query(PostQueryPrefix+"where owner=? and status=?", orgID, models.StatusPublished)
} else {
rows, err = db.Conn.Query(PostQueryPrefix+"where owner=? and type=? and status=?", orgID, tp, models.StatusPublished)
}
if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
posts := GetPosts(user, rows)
sort.Sort(posts)
pinned := make([]*models.Story, 0)
unpinned := make([]*models.Story, 0)
for _, post := range posts {
post.Pinned = GetPinned(post.ID, user.ID)
if post.Pinned {
pinned = append(pinned, post)
} else {
unpinned = append(unpinned, post)
}
}
newPosts := append(pinned, unpinned...)
return newPosts, nil
}
func UserDrafts(user *models.User, uid string) (models.Stories, *e.Error) { func UserDrafts(user *models.User, uid string) (models.Stories, *e.Error) {
rows, err := db.Conn.Query(PostQueryPrefix+"where creator=? and status=?", uid, models.StatusDraft) rows, err := db.Conn.Query(PostQueryPrefix+"where creator=? and status=?", uid, models.StatusDraft)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {

@ -12,11 +12,16 @@ import (
"github.com/imdotdev/im.dev/server/pkg/db" "github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e" "github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models" "github.com/imdotdev/im.dev/server/pkg/models"
"github.com/imdotdev/im.dev/server/pkg/utils"
) )
func GetUsers(q string) ([]*models.User, *e.Error) { func GetUsers(q string) ([]*models.User, *e.Error) {
users := make([]*models.User, 0) users := make([]*models.User, 0)
for _, u := range models.UsersCache { for _, u := range models.UsersCache {
if u.Type != models.IDTypeUser {
continue
}
if strings.HasPrefix(strings.ToLower(u.Nickname), strings.ToLower(q)) { if strings.HasPrefix(strings.ToLower(u.Nickname), strings.ToLower(q)) {
users = append(users, u) users = append(users, u)
continue continue
@ -142,3 +147,52 @@ func NameExist(name string) (bool, *e.Error) {
return true, nil return true, nil
} }
func EmailExist(email string) (bool, *e.Error) {
var ne string
err := db.Conn.QueryRow("SELECT email FROM user WHERE email=?", email).Scan(&ne)
if err != nil && err != sql.ErrNoRows {
logger.Warn("check email exist error", "error", err)
return false, e.New(http.StatusInternalServerError, e.Internal)
}
if err == sql.ErrNoRows {
return false, nil
}
return true, nil
}
func SubmitUser(user *models.User) *e.Error {
if user.Nickname == "" {
user.Nickname = "New user"
}
var err error
now := time.Now()
if user.ID == "" {
// create user
emailExist, err0 := EmailExist(user.Email)
if err0 != nil {
return e.New(err0.Status, err0.Message)
}
if emailExist {
return e.New(http.StatusConflict, "邮箱地址已存在")
}
user.ID = utils.GenID(models.IDTypeUser)
_, err = db.Conn.Exec("INSERT INTO user (id,type,email,username,nickname,role,created,updated) VALUES (?,?,?,?,?,?,?,?)",
user.ID, models.IDTypeUser, user.Email, user.Username, user.Nickname, user.Role, now, now)
} else {
// update user
_, err = db.Conn.Exec("UPDATE user SET role=?,updated=? WHERE id=?", user.Role, now, user.ID)
}
if err != nil {
logger.Warn("submit user error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}

@ -12,11 +12,12 @@ interface Props {
showPinned?: boolean showPinned?: boolean
type?: string type?: string
highlight?: string highlight?: string
showOrg?:boolean
} }
export const Stroies = (props: Props) => { export const Stroies = (props: Props) => {
const { stories,card=StoryCard,showFooter=true,type="classic",showPinned = false} = props const { stories,card=StoryCard,showFooter=true,type="classic",showPinned = false,showOrg=true} = props
const borderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) const borderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark)
const Card = card const Card = card
const showBorder = i => { const showBorder = i => {
@ -35,7 +36,7 @@ export const Stroies = (props: Props) => {
<VStack alignItems="left"> <VStack alignItems="left">
{stories.map((story,i) => {stories.map((story,i) =>
<Box py="2" borderBottom={showBorder(i)? `1px solid ${borderColor}`:null} key={story.id} px="1"> <Box py="2" borderBottom={showBorder(i)? `1px solid ${borderColor}`:null} key={story.id} px="1">
<Card story={story} size={props.size} type={type} highlight={props.highlight} showPinned={showPinned}/> <Card story={story} size={props.size} type={type} highlight={props.highlight} showPinned={showPinned} showOrg={showOrg}/>
</Box>)} </Box>)}
</VStack> </VStack>
{showFooter && <Center><Text layerStyle="textSecondary" fontSize="sm" py="4"></Text></Center>} {showFooter && <Center><Text layerStyle="textSecondary" fontSize="sm" py="4"></Text></Center>}

@ -13,15 +13,16 @@ type Props = PropsOf<typeof chakra.div> & {
size?: 'lg' | 'md' size?: 'lg' | 'md'
story : Story story : Story
showFooter?: boolean showFooter?: boolean
showOrg?: boolean
} }
export const StoryAuthor= ({story,showFooter=true,size='lg'}:Props) =>{ export const StoryAuthor= ({story,showFooter=true,size='lg',showOrg=true}:Props) =>{
const router = useRouter() const router = useRouter()
return ( return (
<HStack spacing="4"> <HStack spacing="4">
<Avatar src={story.creator.avatar} size={size} onClick={() => router.push(`/${story.creator.username}`)} cursor="pointer"/> <Avatar src={story.creator.avatar} size={size} onClick={() => router.push(`/${story.creator.username}`)} cursor="pointer"/>
<VStack alignItems="left" spacing="1"> <VStack alignItems="left" spacing="1">
{story.ownerId ? {(showOrg && story.ownerId!=='') ?
<HStack spacing={size==='lg'?2:1}> <HStack spacing={size==='lg'?2:1}>
<Link href={`/${story.creator.username}`}><Text cursor="pointer">{story.creator.nickname}</Text></Link> <Link href={`/${story.creator.username}`}><Text cursor="pointer">{story.creator.nickname}</Text></Link>
<Text layerStyle="textSecondary">for</Text> <Text layerStyle="textSecondary">for</Text>

@ -16,6 +16,7 @@ interface Props {
story: Story story: Story
type?: string type?: string
highlight?: string highlight?: string
showOrg?: boolean
} }
@ -24,10 +25,9 @@ export const StoryCard = (props: Props) => {
const [isLargeScreen] = useMediaQuery("(min-width: 768px)") const [isLargeScreen] = useMediaQuery("(min-width: 768px)")
const Layout = isLargeScreen ? HStack : VStack const Layout = isLargeScreen ? HStack : VStack
return ( return (
<VStack alignItems="left" spacing={type === "classic" ? 4 : 2} p="2"> <VStack alignItems="left" spacing={type === "classic" ? 4 : 2} p="2">
<StoryAuthor story={story} showFooter={false} size="md" /> <StoryAuthor story={story} showFooter={false} size="md" showOrg={props.showOrg}/>
<a href={getStoryUrl(story)} target="_blank"> <a href={getStoryUrl(story)} target="_blank">
<Layout alignItems={isLargeScreen ? "top" : "left"} cursor="pointer" pl="2" pt="1"> <Layout alignItems={isLargeScreen ? "top" : "left"} cursor="pointer" pl="2" pt="1">
<VStack alignItems="left" spacing={type === "classic" ? 3 : 2} width={isLargeScreen && type === "classic" ? "calc(100% - 15rem)" : '100%'}> <VStack alignItems="left" spacing={type === "classic" ? 3 : 2} width={isLargeScreen && type === "classic" ? "calc(100% - 15rem)" : '100%'}>

@ -70,6 +70,12 @@ export const adminLinks: Route[] = [{
icon: getSvgIcon("navbar"), icon: getSvgIcon("navbar"),
disabled: false disabled: false
}, },
{
title: '用户管理',
path: `${ReserveUrls.Admin}/users`,
icon: getSvgIcon("user"),
disabled: false
},
] ]

@ -1,4 +1,8 @@
import { config } from 'configs/config'
import {User} from 'src/types/user' import {User} from 'src/types/user'
import { requestApi } from './axios/request'
var validator = require('validator');
export function getUserName(user:User) { export function getUserName(user:User) {
return user.nickname === "" ? user.username : user.nickname return user.nickname === "" ? user.username : user.nickname
} }
@ -12,3 +16,83 @@ export function isUsernameChar(c) {
} }
export const usernameInvalidTips = "May only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen." export const usernameInvalidTips = "May only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen."
export const validateUsername = async value => {
let error
if (!value?.trim()) {
return "不能为空"
}
if (value?.length > config.user.usernameMaxLen) {
return `长度不能超过${config.user.usernameMaxLen}`
}
for (const c of value) {
if (!isUsernameChar(c)) {
return usernameInvalidTips
}
}
const res = await requestApi.get(`/user/name/exist/${value}`)
if (res.data) {
return `The name '${value}' is already taken.`
}
return error
}
export function validateNickname(value) {
let error
if (!value?.trim()) {
error = "不能为空"
}
if (value?.length > config.user.usernameMaxLen) {
error = `长度不能超过${config.user.usernameMaxLen}`
}
return error
}
export async function validateEmail(value) {
let email = value?.trim()
if (!email) {
return "邮箱不能为空"
}
if (email?.length > config.user.usernameMaxLen) {
return `长度不能超过${config.user.usernameMaxLen}`
}
if (email) {
if (!validator.isEmail(email)) {
return "Email格式不合法"
}
}
const res = await requestApi.get(`/user/email/exist/${value}`)
if (res.data) {
return `The email '${value}' is already taken.`
}
}
export function validateUrl(value, canBeEmpty = true) {
let url = value?.trim()
let error
if (!canBeEmpty) {
if (!url) {
error = "url不能为空"
return error
}
}
if (url) {
if (!validator.isURL(value)) {
error = "URL格式不合法"
return error
}
}
return error
}
Loading…
Cancel
Save