pull/50/head
sunface 4 years ago
parent 2046b95337
commit c6adb3a813

@ -11,8 +11,10 @@ server:
#################################### User/Session ##############################
user:
# github username
# username for superadmin, setted only in first run, can't be changed
super_admin_username: "sunface"
# email for superadmin
super_admin_email: "cto@188.com"
# a session is created when user login to im.dev, this session will be expired after X seconds
session_expire: 259200

@ -93,11 +93,11 @@ function HeaderContent() {
</Link>
<DarkMode />
<AccountMenu />
<MobileNavButton
{/* <MobileNavButton
ref={mobileNavBtnRef}
aria-label="Open Menu"
onClick={mobileNav.onOpen}
/>
/> */}
</HStack>
</Flex>
<MobileNavContent isOpen={mobileNav.isOpen} onClose={mobileNav.onClose} />

@ -0,0 +1,136 @@
import {
chakra,
Flex,
HStack,
IconButton,
useColorModeValue,
useDisclosure,
useUpdateEffect,
Box,
VStack,
useMediaQuery
} from "@chakra-ui/react"
import siteConfig from "configs/site-config"
import { useViewportScroll } from "framer-motion"
import NextLink from "next/link"
import React from "react"
import { FaGithub, FaSearch } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo"
import { MobileNavButton, MobileNavContent } from "./mobile-nav"
import AlgoliaSearch from "src/components/search/algolia-search"
import { useRouter } from "next/router"
import { ReserveUrls } from "src/data/reserve-urls"
import Link from "next/link"
import DarkMode from "components/dark-mode"
import AccountMenu from "components/account-menu"
const navLinks = [{
title: '主页',
url: '/',
},
{
title: '标签',
url: ReserveUrls.Tags,
},
{
title: '学习资料',
url: ReserveUrls.Courses,
},
]
function HeaderContent() {
const router = useRouter()
const { asPath } = router
const mobileNav = useDisclosure()
const mobileNavBtnRef = React.useRef<HTMLButtonElement>()
const [isLargerThan768] = useMediaQuery("(min-width: 768px)")
useUpdateEffect(() => {
mobileNavBtnRef.current?.focus()
}, [mobileNav.isOpen])
return (
<>
<Flex className="vertical-nav" h="100%" align="center" justify="space-between" px={[2,2,6,6]} direction="column" py="8">
<VStack align="center">
<NextLink href="/" passHref>
<chakra.a style={{ marginTop: '-5px' }} aria-label="Chakra UI, Back to homepage">
{isLargerThan768 ? <Logo width="130" /> : <Logo width="105" />}
</chakra.a>
</NextLink>
<VStack pt="6" ml={{ base: 1, md: 4, lg: 12 }} fontSize="1rem" minWidth="250px">
{navLinks.map(link => <Box px="4" py="0.7rem" rounded="md" key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={asPath === link.url ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} ><Link href={link.url}>{link.title}</Link></Box>)}
</VStack>
</VStack>
<VStack
spacing="4"
align="center"
color={useColorModeValue("gray.500", "gray.400")}
>
<IconButton
fontSize="1.4rem"
aria-label="go to github"
variant="ghost"
color="current"
_focus={null}
icon={<FaSearch />}
/>
<Link
aria-label="Go to Chakra UI GitHub page"
href={siteConfig.repo.url}
>
<IconButton
size="md"
fontSize="1.4rem"
aria-label="go to github"
variant="ghost"
color="current"
_focus={null}
icon={<FaGithub />}
/>
</Link>
<DarkMode fontSize="1.4rem"/>
<AccountMenu />
{/* <MobileNavButton
ref={mobileNavBtnRef}
aria-label="Open Menu"
onClick={mobileNav.onOpen}
/> */}
</VStack>
</Flex>
<MobileNavContent isOpen={mobileNav.isOpen} onClose={mobileNav.onClose} />
</>
)
}
function VerticalNav(props) {
const ref = React.useRef<HTMLHeadingElement>()
return (
<chakra.header
ref={ref}
transition="box-shadow 0.2s"
pos="fixed"
top="0"
zIndex="3"
left="0"
bottom="0"
bg={useColorModeValue('white', 'gray.800')}
{...props}
>
<chakra.div height="100%">
<HeaderContent />
</chakra.div>
</chakra.header>
)
}
export default VerticalNav

@ -1,4 +1,4 @@
import { Box, Stack } from "@chakra-ui/react"
import { Box, Heading, Stack, VStack } from "@chakra-ui/react"
import Card from "components/card"
import { useRouter } from "next/router"
import * as React from "react"
@ -22,11 +22,13 @@ export function SidebarContent(props) {
)
}
const Sidebar = ({ routes, ...props }) => {
const Sidebar = ({ routes,title, ...props }) => {
const { pathname } = useRouter()
const ref = React.useRef<HTMLDivElement>(null)
return (
<VStack alignItems="left">
<Card p="5"><Heading size="sm">{title}</Heading></Card>
<Card p="0" {...props}>
<Box
ref={ref}
@ -36,7 +38,7 @@ const Sidebar = ({ routes, ...props }) => {
sx={{
overscrollBehavior: "contain",
}}
top="6.5rem"
top="8.5rem"
pr="3"
pb="6"
pl="3"
@ -44,11 +46,12 @@ const Sidebar = ({ routes, ...props }) => {
overflowY="auto"
className="sidebar-content"
flexShrink={0}
display={{ base: "none", md: "block" }}
// display={{ base: "none", md: "block" }}
>
<SidebarContent routes={routes} pathname={pathname} contentRef={ref} />
</Box>
</Card>
</VStack>
)
}

@ -1,23 +1,58 @@
import { chakra } from "@chakra-ui/react"
import { Box, chakra, Flex, HStack, VStack ,Image, Heading, Text, Button, useColorModeValue} from "@chakra-ui/react"
import Card from "components/card"
import Container from "components/container"
import SEO from "components/seo"
import siteConfig from "configs/site-config"
import useSession from "hooks/use-session"
import Nav from "layouts/nav/nav"
import VerticalNav from "layouts/nav/vertical-nav"
import PageContainer from "layouts/page-container"
import { useRouter } from "next/router"
import React from "react"
import React, { useEffect, useState } from "react"
import { FaEdit, FaPlus } from "react-icons/fa"
import { User } from "src/types/session"
import { requestApi } from "utils/axios/request"
const UserPage = () => {
const router = useRouter()
const username = router.query.username
const session = useSession()
const [user,setUser]:[User,any] = useState(null)
const borderColor = useColorModeValue('white','transparent')
useEffect(() => {
if (username) {
requestApi.get(`/user/info/${username}`).then(res => setUser(res.data))
}
},[username])
return (
<>
<SEO
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<PageContainer mt="6rem">
<chakra.h1>{router.query.username}'s home</chakra.h1>
</PageContainer>
<Flex px={[0,0,16,16]}>
<VerticalNav width={["100px","100px","200px","200px"]}/>
<Box width="100%" ml={["100px","100px","200px","200px"]}>
{
user &&
<VStack alignItems="left">
<Card p="0">
<Box backgroundImage={`url(${user.cover})`} height="300px" width="100%" backgroundSize="cover" backgroundPosition="center"/>
<VStack maxHeight="190px">
<Image src={user.avatar} height="140px" borderRadius="50%" border={`4px solid ${borderColor}`} position="relative" top="-66px" left="-140px"/>
<Box position="relative" top="-130px">
<Heading fontSize="1.8rem">{user.nickname}</Heading>
<Text layerStyle="textSecondary" fontWeight="450" fontSize="1.3rem" ml="1" mt="2">{user.tagline}</Text>
{session?.user.id === user.id ?
<Button variant="ghost" ml="-15px" mt="6" leftIcon={<svg height="1.3rem" fill="currentColor" viewBox="0 0 512 512"><path d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zm-95.196 140.45L174 420.745V386h-48v-48H91.255l224.059-224.059 82.745 82.745zM126.147 468.598l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>}>Edit Profile</Button>
:<Button variant="outline" leftIcon={<FaPlus />} colorScheme="teal" mt="6">Follow</Button>}
</Box>
</VStack>
</Card>
</VStack>
}
</Box>
</Flex>
</>
)}

@ -46,7 +46,7 @@ const PostsPage = () => {
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={adminLinks} width="250px" height="fit-content" />
<Sidebar routes={adminLinks} width="250px" height="fit-content" title="管理员" />
<Card ml="4" p="6" width="100%">
<Flex alignItems="center" justify="space-between">
<Heading size="md">({tags.length})</Heading>

@ -99,7 +99,7 @@ const PostsPage = () => {
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={editorLinks} width="250px" height="fit-content" />
<Sidebar routes={editorLinks} width="250px" height="fit-content" title="创作中心"/>
<Card ml="4" p="6" width="100%">
<Flex alignItems="center" justify="space-between">
<Heading size="md">({posts.length})</Heading>

@ -0,0 +1,298 @@
import { Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, HStack, Wrap, useMediaQuery, Avatar, Textarea, } 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 { adminLinks, settingLinks } from "src/data/links"
import { requestApi } from "utils/axios/request"
import TagCard from "components/posts/tag-edit-card"
import { Post } from "src/types/posts"
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 { Field, Form, Formik } from "formik"
import useSession from "hooks/use-session"
import { config } from "utils/config"
var validator = require('validator');
const UserProfilePage = () => {
const [user, setUser] = useState(null)
const [isLargerThan1280] = useMediaQuery("(min-width: 768px)")
useEffect(() => {
requestApi.get("/user/self").then(res => setUser(res.data))
}, [])
const router = useRouter()
const toast = useToast()
const submitUser = async (values, _) => {
await requestApi.post(`/user/update`,values)
setUser(values)
toast({
description: "更新成功",
status: "success",
duration: 2000,
isClosable: true,
})
}
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) {
let error
if (value?.length > config.commonMaxlen) {
error = `长度不能超过${config.commonMaxlen}`
}
return error
}
const Layout = isLargerThan1280 ? HStack : VStack
return (
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={settingLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="偏好设置" />
{user && <VStack alignItems="left" ml="4" width="100%">
<Formik
initialValues={user}
onSubmit={submitUser}
>
{(props) => (
<Form>
<Card p={[2, 2, 6, 6]}>
<Layout spacing={isLargerThan1280 ? "8" : "6"} alignItems={isLargerThan1280 ? 'top' : 'left'}>
<Box width="100%">
<VStack alignItems="left" spacing="6">
<Heading fontSize="1.2rem"></Heading>
<Field name="nickname" validate={validateNickname}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.nickname && form.touched.nickname} >
<FormLabel></FormLabel>
<Input {...field} placeholder="enter your nick name" size="lg" />
<FormErrorMessage>{form.errors.nickname}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="email" validate={validateEmail}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.email && form.touched.email} >
<FormLabel></FormLabel>
<Input {...field} placeholder="" size="lg" />
<FormErrorMessage>{form.errors.email}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="avatar" validate={(v) => validateUrl(v, false)}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.avatar && form.touched.avatar} >
<FormLabel></FormLabel>
<Input {...field} placeholder="输入图片链接可以用github或postimg.cc当图片存储服务" size="lg" />
<FormErrorMessage>{form.errors.avatar}</FormErrorMessage>
{user.avatar && <Image width="120px" mt="4" src={user.avatar} />}
</FormControl>
)}
</Field>
<Field name="cover" validate={(v) => validateUrl(v, true)}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.cover && form.touched.cover} >
<FormLabel></FormLabel>
<Input {...field} placeholder="输入图片链接" size="lg" />
<FormErrorMessage>{form.errors.cover}</FormErrorMessage>
{user.cover && <Image width="100%" mt="4" src={user.cover} />}
</FormControl>
)}
</Field>
</VStack>
<VStack alignItems="left" spacing="6" mt="6">
<Heading fontSize="1.2rem">About You</Heading>
<Field name="tagline" validate={validateLen}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.tagline && form.touched.tagline} >
<FormLabel></FormLabel>
<Input {...field} placeholder="I'm dev, working for google now" size="lg" />
<FormErrorMessage>{form.errors.tagline}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="location" validate={validateLen}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.location && form.touched.location} >
<FormLabel>Location</FormLabel>
<Input {...field} placeholder="Califonia, US" size="lg" />
<FormErrorMessage>{form.errors.location}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="availFor" validate={validateLen}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.availFor && form.touched.availFor} >
<FormLabel>Available for</FormLabel>
<Textarea {...field} placeholder="I'm available for ..." size="lg" />
<FormErrorMessage>{form.errors.availFor}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="about" validate={validateLen}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.about && form.touched.about} >
<FormLabel></FormLabel>
<Textarea {...field} placeholder="give us more info about you" size="lg" />
<FormErrorMessage>{form.errors.about}</FormErrorMessage>
</FormControl>
)}
</Field>
</VStack>
</Box>
<Box width="100%" >
<VStack alignItems="left" spacing="6">
<Heading fontSize="1.2rem"></Heading>
<Field name="website" validate={validateUrl}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.website && form.touched.website} >
<FormLabel></FormLabel>
<Input {...field} placeholder="https://sunface.dev" size="lg" />
<FormErrorMessage>{form.errors.website}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="github" validate={validateUrl}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.github && form.touched.github} >
<FormLabel>Github</FormLabel>
<Input {...field} placeholder="https://github.com/sunface" size="lg" />
<FormErrorMessage>{form.errors.github}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="twitter" validate={validateUrl}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.twitter && form.touched.twitter} >
<FormLabel>Twitter</FormLabel>
<Input {...field} placeholder="https://twitter.com/imdotdev" size="lg" />
<FormErrorMessage>{form.errors.twitter}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="zhihu" validate={validateUrl}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.zhihu && form.touched.zhihu} >
<FormLabel></FormLabel>
<Input {...field} placeholder="https://www.zhihu.com/people/iSunface" size="lg" />
<FormErrorMessage>{form.errors.zhihu}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="weibo" validate={validateUrl}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.weibo && form.touched.weibo} >
<FormLabel></FormLabel>
<Input {...field} placeholder="https://weibo.com/2734382464" size="lg" />
<FormErrorMessage>{form.errors.weibo}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="facebook" validate={validateUrl}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.facebook && form.touched.facebook} >
<FormLabel>Facebook</FormLabel>
<Input {...field} placeholder="" size="lg" />
<FormErrorMessage>{form.errors.facebook}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="stackoverflow" validate={validateUrl}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.stackoverflow && form.touched.stackoverflow} >
<FormLabel>StackOverflow</FormLabel>
<Input {...field} placeholder="" size="lg" />
<FormErrorMessage>{form.errors.stackoverflow}</FormErrorMessage>
</FormControl>
)}
</Field>
</VStack>
</Box>
</Layout>
</Card>
<Box mt={6}>
<Button
// colorScheme="teal"
// variant="outline"
type="submit"
_focus={null}
layerStyle="colorButton"
>
</Button>
</Box>
</Form>
)}
</Formik>
</VStack>}
</Box>
</PageContainer>
</>
)
}
export default UserProfilePage

@ -0,0 +1 @@
<svg height="1.5rem" fill="currentColor" viewBox="0 0 512 512"><path d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zm-95.196 140.45L174 420.745V386h-48v-48H91.255l224.059-224.059 82.745 82.745zM126.147 468.598l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>

After

Width:  |  Height:  |  Size: 612 B

@ -5,8 +5,8 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/internal/story"
"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"
@ -32,7 +32,7 @@ func SubmitComment(c *gin.Context) {
var err *e.Error
if comment.ID == "" { //add comment
user := session.CurrentUser(c)
user := user.CurrentUser(c)
comment.CreatorID = user.ID
comment.ID = utils.GenStoryID(models.StoryComment)
err = story.AddComment(comment)
@ -56,7 +56,7 @@ func GetStoryComments(c *gin.Context) {
return
}
user := session.CurrentUser(c)
user := user.CurrentUser(c)
for _, comment := range comments {
if user != nil {
comment.Liked = story.GetLiked(comment.ID, user.ID)
@ -87,7 +87,7 @@ func DeleteComment(c *gin.Context) {
return
}
user := session.CurrentUser(c)
user := user.CurrentUser(c)
canDel := false
if user.Role.IsAdmin() {
canDel = true

@ -4,14 +4,14 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/internal/story"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e"
)
func GetEditorPosts(c *gin.Context) {
user := session.CurrentUser(c)
user := user.CurrentUser(c)
ars, err := story.UserPosts(int64(user.ID))
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
@ -28,7 +28,7 @@ func GetEditorPost(c *gin.Context) {
return
}
user := session.CurrentUser(c)
user := user.CurrentUser(c)
creator, err := story.GetPostCreator(id)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))

@ -4,8 +4,8 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/internal/story"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e"
)
@ -27,7 +27,7 @@ func DeletePost(c *gin.Context) {
return
}
user := session.CurrentUser(c)
user := user.CurrentUser(c)
creator, err := story.GetPostCreator(id)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
@ -56,7 +56,7 @@ func GetPost(c *gin.Context) {
return
}
user := session.CurrentUser(c)
user := user.CurrentUser(c)
if user == nil {
ar.Liked = false
} else {
@ -68,7 +68,7 @@ func GetPost(c *gin.Context) {
}
func LikeStory(c *gin.Context) {
user := session.CurrentUser(c)
user := user.CurrentUser(c)
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))

@ -5,8 +5,8 @@ import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/internal/tags"
"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"
@ -34,7 +34,7 @@ func GetTags(c *gin.Context) {
}
func SubmitTag(c *gin.Context) {
user := session.CurrentUser(c)
user := user.CurrentUser(c)
if !user.Role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoEditorPermission))
return
@ -64,7 +64,7 @@ func DeleteTag(c *gin.Context) {
return
}
user := session.CurrentUser(c)
user := user.CurrentUser(c)
if !user.Role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
}

@ -4,13 +4,15 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/session"
"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 GetUsers(c *gin.Context) {
query := c.Query("query")
users, err := session.GetUsers(query)
users, err := user.GetUsers(query)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
@ -18,3 +20,51 @@ func GetUsers(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(users))
}
func GetUserSelf(c *gin.Context) {
u := user.CurrentUser(c)
userDetail, err := user.GetUserDetail(u.ID, "")
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(userDetail))
}
func GetUser(c *gin.Context) {
username := c.Param("username")
userDetail, err := user.GetUserDetail(0, username)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(userDetail))
}
func UpdateUser(c *gin.Context) {
u := &models.User{}
c.Bind(&u)
currentUser := user.CurrentUser(c)
if currentUser.ID != u.ID {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
err := user.UpdateUser(u)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func GetSession(c *gin.Context) {
sess := user.GetSession(c)
c.JSON(http.StatusOK, common.RespSuccess(sess))
}

@ -6,8 +6,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/api"
"github.com/imdotdev/im.dev/server/internal/cache"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/internal/storage"
"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/e"
@ -44,8 +44,8 @@ func (s *Server) Start() error {
r := router.Group("/api")
{
r.POST("/login", session.Login)
r.POST("/logout", session.Logout)
r.POST("/login", user.Login)
r.POST("/logout", user.Logout)
r.GET("/uiconfig", GetUIConfig)
}
@ -69,6 +69,10 @@ func (s *Server) Start() error {
r.GET("/tag/:name", api.GetTag)
r.GET("/users", api.GetUsers)
r.GET("/user/self", IsLogin(), api.GetUserSelf)
r.GET("/user/info/:username", api.GetUser)
r.POST("/user/update", IsLogin(), api.UpdateUser)
r.GET("/session", IsLogin(), api.GetSession)
err := router.Run(config.Data.Server.Addr)
if err != nil {
logger.Crit("start backend server error", "error", err)
@ -107,7 +111,7 @@ func Cors() gin.HandlerFunc {
// Auth is a gin middleware for user auth
func IsLogin() gin.HandlerFunc {
return func(c *gin.Context) {
user := session.CurrentUser(c)
user := user.CurrentUser(c)
if user == nil {
c.JSON(http.StatusUnauthorized, common.RespError(e.NeedLogin))
c.Abort()

@ -1,28 +0,0 @@
package session
import (
"strings"
"github.com/imdotdev/im.dev/server/internal/cache"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
)
func GetUsers(q string) ([]*models.User, *e.Error) {
allUsers := cache.Users
users := make([]*models.User, 0)
for _, u := range allUsers {
if strings.HasPrefix(strings.ToLower(u.Nickname), strings.ToLower(q)) {
users = append(users, u)
continue
}
if strings.HasPrefix(strings.ToLower(u.Username), strings.ToLower(q)) {
users = append(users, u)
continue
}
}
return users, nil
}

@ -56,8 +56,8 @@ func initTables() error {
}
now := time.Now()
_, err := db.Conn.Exec(`INSERT INTO user (id,username,role,nickname,avatar,created,updated) VALUES (?,?,?,?,?,?,?)`,
1, config.Data.User.SuperAdminUsername, models.ROLE_SUPER_ADMIN, "", "", now, now)
_, err := db.Conn.Exec(`INSERT INTO user (id,username,email,role,nickname,avatar,created,updated) VALUES (?,?,?,?,?,?,?,?)`,
1, config.Data.User.SuperAdminUsername, config.Data.User.SuperAdminEmail, models.ROLE_SUPER_ADMIN, "", "", now, now)
if err != nil {
log.RootLogger.Crit("init super admin error", "error:", err)
return err

@ -6,7 +6,7 @@ var sqlTables = map[string]string{
username VARCHAR(255) NOT NULL UNIQUE,
nickname VARCHAR(255) DEFAULT '',
avatar VARCHAR(255) DEFAULT '',
email VARCHAR(255) UNIQUE DEFAULT '',
email VARCHAR(255) UNIQUE NOT NULL,
role VARCHAR(20) NOT NULL,
last_seen_at DATETIME DEFAULT CURRENT_DATETIME,
@ -22,6 +22,26 @@ var sqlTables = map[string]string{
CREATE INDEX IF NOT EXISTS user_email
ON user (email);`,
"user_profile": `CREATE TABLE IF NOT EXISTS user_profile (
id INTEGER PRIMARY KEY,
tagline VARCHAR(255),
cover VARCHAR(255),
location VARCHAR(255),
avail_for TEXT,
about TEXT,
website VARCHAR(255),
twitter VARCHAR(255),
github VARCHAR(255),
zhihu VARCHAR(255),
weibo VARCHAR(255),
facebook VARCHAR(255),
stackoverflow VARCHAR(255),
updated DATETIME
);`,
"sessions": `CREATE TABLE IF NOT EXISTS sessions (
sid VARCHAR(255) primary key,
user_id INTEGER

@ -89,7 +89,14 @@ func GetComment(id string) (*models.Comment, *e.Error) {
}
func DeleteComment(id string) *e.Error {
_, err := db.Conn.Exec("DELETE FROM comments WHERE id=?", id)
// delete children replies
_, err := db.Conn.Exec("DELETE FROM comments WHERE target_id=?", id)
if err != nil {
logger.Warn("delete comment replies error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
_, err = db.Conn.Exec("DELETE FROM comments WHERE id=?", id)
if err != nil {
logger.Warn("delete comment error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)

@ -11,8 +11,8 @@ import (
"github.com/asaskevich/govalidator"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/internal/tags"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/config"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
@ -50,7 +50,7 @@ func UserPosts(uid int64) (models.Posts, *e.Error) {
}
func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
user := session.CurrentUser(c)
user := user.CurrentUser(c)
post := &models.Post{}
err := c.Bind(&post)

@ -9,8 +9,9 @@ import (
)
type UIConfig struct {
Posts *PostsConfig `json:"posts"`
User *UserConfig `json:"user"`
CommonMaxLen int `json:"commonMaxlen"`
Posts *PostsConfig `json:"posts"`
User *UserConfig `json:"user"`
}
type PostsConfig struct {
@ -28,6 +29,7 @@ type UserConfig struct {
// 在后台页面配置存储到mysql中
func GetUIConfig(c *gin.Context) {
conf := &UIConfig{
CommonMaxLen: 255,
Posts: &PostsConfig{
TitleMaxLen: config.Data.Posts.TitleMaxLen,
BriefMaxLen: config.Data.Posts.BriefMaxLen,

@ -1,4 +1,4 @@
package session
package user
import (
"database/sql"
@ -111,6 +111,26 @@ func CurrentUser(c *gin.Context) *models.User {
return sess.User
}
func GetSession(c *gin.Context) *Session {
token := getToken(c)
createTime, _ := strconv.ParseInt(token, 10, 64)
if createTime != 0 {
// check whether token is expired
if (time.Now().Unix() - createTime/1e9) > config.Data.User.SessionExpire {
deleteSession(token)
return nil
}
}
sess := loadSession(token)
if sess == nil {
// 用户未登陆或者session失效
return nil
}
return sess
}
func loadSession(sid string) *Session {
var userid int64
q := `SELECT user_id FROM sessions WHERE sid=?`

@ -0,0 +1,86 @@
package user
import (
"database/sql"
"net/http"
"strings"
"time"
"github.com/imdotdev/im.dev/server/internal/cache"
"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(q string) ([]*models.User, *e.Error) {
allUsers := cache.Users
users := make([]*models.User, 0)
for _, u := range allUsers {
if strings.HasPrefix(strings.ToLower(u.Nickname), strings.ToLower(q)) {
users = append(users, u)
continue
}
if strings.HasPrefix(strings.ToLower(u.Username), strings.ToLower(q)) {
users = append(users, u)
continue
}
}
return users, nil
}
func GetUserDetail(id int64, username string) (*models.User, *e.Error) {
user := &models.User{}
err := user.Query(id, username, "")
if err != nil {
logger.Warn("query user error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
err = db.Conn.QueryRow("SELECT tagline,cover,location,avail_for,about,website,twitter,github,zhihu,weibo,facebook,stackoverflow from user_profile WHERE id=?", user.ID).Scan(
&user.Tagline, &user.Cover, &user.Location, &user.AvailFor, &user.About, &user.Website, &user.Twitter,
&user.Github, &user.Zhihu, &user.Weibo, &user.Facebook, &user.Stackoverflow,
)
if err != nil && err != sql.ErrNoRows {
logger.Warn("query user profile error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
return user, nil
}
func UpdateUser(u *models.User) *e.Error {
_, err := db.Conn.Exec("UPDATE user SET nickname=?,avatar=?,email=?,updated=? WHERE id=?", u.Nickname, u.Avatar, u.Email, time.Now(), u.ID)
if err != nil {
if e.IsErrUniqueConstraint(err) {
return e.New(http.StatusConflict, "email已经存在")
}
logger.Warn("update user error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
var nid int64
err = db.Conn.QueryRow("SELECT id FROM user_profile WHERE id=?", u.ID).Scan(&nid)
if err != nil && err != sql.ErrNoRows {
logger.Warn("update user profile error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
now := time.Now()
if err == sql.ErrNoRows {
_, err = db.Conn.Exec("INSERT INTO user_profile (id,tagline,cover,location,avail_for,about,website,twitter,github,zhihu,weibo,facebook,stackoverflow,updated) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
u.ID, u.Tagline, u.Cover, u.Location, u.AvailFor, u.About, u.Website, u.Twitter, u.Github, u.Zhihu, u.Weibo, u.Facebook, u.Stackoverflow, now)
} else {
_, err = db.Conn.Exec("UPDATE user_profile SET tagline=?,cover=?,location=?,avail_for=?,about=?,website=?,twitter=?,github=?,zhihu=?,weibo=?,facebook=?,stackoverflow=?,updated=? WHERE id=?",
u.Tagline, u.Cover, u.Location, u.AvailFor, u.About, u.Website, u.Twitter, u.Github, u.Zhihu, u.Weibo, u.Facebook, u.Stackoverflow, now, u.ID)
}
if err != nil {
logger.Warn("update user profile error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}

@ -19,6 +19,7 @@ type Config struct {
User struct {
SuperAdminUsername string `yaml:"super_admin_username"`
SuperAdminEmail string `yaml:"super_admin_email"`
SessionExpire int64 `yaml:"session_expire"`
}

@ -7,12 +7,28 @@ import (
)
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Email string `json:"email"`
Role RoleType `json:"role"`
ID int64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Email string `json:"email"`
Role RoleType `json:"role"`
Tagline string `json:"tagline"`
Cover string `json:"cover"`
Location string `json:"location"`
AvailFor string `json:"availFor"`
About string `json:"about"`
Skills string `json:"skills"`
Website string `json:"website"`
Twitter string `json:"twitter"`
Github string `json:"github"`
Zhihu string `json:"zhihu"`
Weibo string `json:"weibo"`
Facebook string `json:"facebook"`
Stackoverflow string `json:"stackoverflow"`
LastSeenAt time.Time `json:"lastSeenAt,omitempty"`
Created time.Time `json:"created"`
}

@ -49,15 +49,17 @@ export const AccountMenu = () => {
ml={{ base: "0", md: "2" }}
/>
<MenuList>
<MenuItem icon={<FaUserAlt fontSize="16" />}>
<span>Sunface</span>
</MenuItem>
<Link href={`/${session.user.username}`}>
<MenuItem icon={<FaUserAlt fontSize="16" />}>
<span>{session.user.nickname}</span>
</MenuItem>
</Link>
<MenuDivider />
{isEditor(session.user.role) && <Link href={`${ReserveUrls.Editor}/posts`}><MenuItem icon={<FaEdit fontSize="16" />} ></MenuItem></Link>}
{isAdmin(session.user.role) && <Link href={`${ReserveUrls.Admin}/tags`}><MenuItem icon={<FaStar fontSize="16" />} ></MenuItem></Link>}
<MenuItem icon={<FaBookmark fontSize="16" />}></MenuItem>
<MenuDivider />
<MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem>
<Link href={`${ReserveUrls.Settings}/profile`}><MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem></Link>
<MenuItem onClick={() => logout()} icon={<FaSignOutAlt fontSize="16" />}></MenuItem>
</MenuList>
</Menu> :

@ -2,7 +2,7 @@ import React from "react"
import { IconButton, useColorMode, useColorModeValue } from "@chakra-ui/react"
import { FaMoon, FaSun } from "react-icons/fa"
export const DarkMode = () => {
export const DarkMode = (props) => {
const { toggleColorMode: toggleMode } = useColorMode()
const text = useColorModeValue("dark", "light")
const SwitchIcon = useColorModeValue(FaMoon, FaSun)
@ -10,7 +10,7 @@ export const DarkMode = () => {
return (
<IconButton
size="md"
fontSize="lg"
fontSize={props.fontSize??'lg'}
aria-label={`Switch to ${text} mode`}
variant="ghost"
color="current"

@ -1,5 +1,5 @@
import React from 'react'
import { FaFileAlt, FaScroll, FaBookOpen, FaTags } from 'react-icons/fa'
import { FaFileAlt, FaScroll, FaBookOpen, FaTags, FaUserCircle } from 'react-icons/fa'
import { Route } from 'src/types/route'
import { ReserveUrls } from './reserve-urls'
export const editorLinks: Route[] = [{
@ -27,5 +27,12 @@ export const adminLinks: Route[] = [{
path: `${ReserveUrls.Admin}/tags`,
icon: <FaTags />,
disabled: false
}
]
}]
export const settingLinks: Route[] = [{
title: '用户设置',
path: `${ReserveUrls.Settings}/profile`,
icon: <FaUserCircle />,
disabled: false
}]

@ -1,5 +1,6 @@
import { useEffect, useState } from "react"
import { Session } from "src/types/session"
import { requestApi } from "utils/axios/request"
import events from "utils/events"
import storage from "utils/localStorage"
@ -9,6 +10,10 @@ function useSession(): Session{
const sess = storage.get('session')
if (sess) {
setSession(sess)
// 页面重新进入时,跟服务器端进行信息同步
requestApi.get(`/session`).then(res => {
setSession(res.data)
})
}
events.on('set-session',storeSession)

@ -1,23 +1,43 @@
import { Tag } from './tag'
export interface Session {
token: string
createTime: string
user : User
token: string
createTime: string
user: User
}
export interface User {
id :number
username: string
nickname: string
avatar: string
role?: string
email?: string
// basic info
id: number
username: string
nickname: string
avatar: string
role?: string
email?: string
// about user
tagline?: string
cover?: string
location?: string
availFor?: string
about?: string
skills?: Tag[]
// social links
website?: string
twitter?: string
github?: string
zhihu?: string
weibo?: string
facebook?: string
stackoverflow?: string
lastSeenAt?: string
created?: string
}
export interface UserSimple {
id :number
username: string
nickname: string
avatar: string
id: number
username: string
nickname: string
avatar: string
}

@ -1,6 +1,7 @@
import { requestApi } from "./axios/request"
export let config = {
commonMaxlen: 255,
posts: {
titleMaxLen: 128,
briefMaxLen: 128,

Loading…
Cancel
Save