pull/52/head
sunface 5 years ago
parent 5084bcb80e
commit 029473b4bc

@ -19,6 +19,9 @@ user:
# a session is created when user login to im.dev, this session will be expired after X seconds
session_expire: 259200
ui:
# base ui domain
domain: "http://localhost:4004"
#################################### Paths ##############################
# Path to where im.dev can store temp files, sessions, and the sqlite3 db (if that is used)

@ -2,6 +2,7 @@ import { requestApi } from "../src/utils/axios/request"
export let config = {
appName: "im.dev",
uiDomain: "http://localhost:4004",
commonMaxlen: 255,
posts: {
titleMaxLen: 128,

@ -92,7 +92,7 @@ function HeaderContent() {
variant="ghost"
color="current"
_focus={null}
display={{ base: "none", md: "block" }}
display={{ base: "none", md: "flex" }}
icon={<FaGithub />}
/>
</Link>

@ -1,4 +1,4 @@
import React from "react"
import React, { useState } from "react"
import {
Text,
Box,
@ -7,20 +7,34 @@ import {
Image,
useColorModeValue,
Link,
Center
Center,
Flex,
IconButton,
HStack,
Modal,
ModalOverlay,
ModalContent,
ModalBody,
useDisclosure,
Input,
useToast
} from "@chakra-ui/react"
import Logo from "components/logo"
import { FaGithub } from "react-icons/fa"
import { FaEnvelope, FaGithub } from "react-icons/fa"
import { requestApi } from "utils/axios/request"
import { saveToken } from "utils/axios/getToken"
import storage from "utils/localStorage"
import { useRouter } from "next/router"
import { validateEmail } from "utils/user"
const LoginPage = () => {
const { isOpen, onOpen, onClose } = useDisclosure()
const toast = useToast()
const router = useRouter()
const login = async () => {
const res = await requestApi.post("/user/login")
const [email,setEmail] = useState('')
const login = async (email:string) => {
const res = await requestApi.post("/user/login",{email: email})
saveToken(res.data.token)
storage.set('session', res.data)
const oldPage = storage.get('current-page')
@ -32,10 +46,25 @@ const LoginPage = () => {
}
}
const onEmailLogin = async () => {
const err = await validateEmail(email,false)
if (err) {
toast({
description: err,
status: "error",
duration: 2000,
isClosable: true,
})
return
}
login(email)
}
return (
<Box height="100vh" width="100%" display="flex" alignItems="center" justifyContent="center">
<Image src="/login-bg.svg" height="100%" position="absolute" />
<Box textAlign="center" display="flex" alignItems="center" flexDirection="column">
<Box textAlign="center" display="flex" alignItems="center" flexDirection="column">
<Logo width="12rem" />
<Text mt="8" fontSize="1.1rem" fontWeight="500">im.dev</Text>
<VStack mt="2" p="5" align="left" spacing="2" fontSize="15px">
@ -53,11 +82,26 @@ const LoginPage = () => {
</Box>
</VStack>
<Button onClick={() => login()} layerStyle="colorButton" mt="6" fontSize=".9rem" leftIcon={<FaGithub fontSize="1.0rem" />}>使github</Button>
<HStack mt="6" spacing="3">
<Button onClick={() => login('cto@188.com')} layerStyle="colorButton" fontSize=".9rem" leftIcon={<FaGithub fontSize="1.0rem" />}>使github</Button>
<Text layerStyle="textSecondary">OR</Text>
<IconButton layerStyle="textSecondary" variant="outline" aria-label="login with email" icon={<FaEnvelope />} onClick={onOpen}/>
</HStack>
<Text mt="6" fontSize=".7rem" layerStyle="textSecondary">im.dev<Link textDecoration="underline"></Link><Link textDecoration="underline"></Link></Text>
{/* <Image src="/pokeman.svg" height="300px" /> */}
</Box>
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay />
<ModalContent mt="0" px="10" py="6">
<ModalBody>
<VStack alignItems="left" spacing="5">
<Text layerStyle="textSecondary" fontWeight="550">Sign in using a secure link</Text>
<Input value={email} onChange={e => setEmail(e.currentTarget.value)} placeholder="enter your email address" _focus={null}></Input>
<Button colorScheme="teal" onClick={onEmailLogin}>Submit</Button>
</VStack>
</ModalBody>
</ModalContent>
</Modal>
</Box>
)
}

@ -1,301 +0,0 @@
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, orgSettingLinks, settingLinks } from "src/data/links"
import { requestApi } from "utils/axios/request"
import { useRouter } from "next/router"
import { Field, Form, Formik } from "formik"
import { config } from "configs/config"
import Tags from "components/tags/tags"
var validator = require('validator');
const UserProfilePage = () => {
const [user, setUser] = useState(null)
const [skills, setSkills] = useState([])
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={orgSettingLinks} 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 size="sm"></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 size="sm">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>
<Field name="skills" validate={validateLen}>
{({ field, form }) => (
<FormControl >
<FormLabel></FormLabel>
<Tags tags={user.skills} onChange={(v) => form.values.skills = v} size="lg"/>
</FormControl>
)}
</Field>
</VStack>
</Box>
<Box width="100%" >
<VStack alignItems="left" spacing="6">
<Heading size="sm"></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="cyan"
variant="outline"
type="submit"
_focus={null}
>
</Button>
</Box>
</Form>
)}
</Formik>
</VStack>}
</Box>
</PageContainer>
</>
)
}
export default UserProfilePage

@ -0,0 +1,119 @@
import { Text, Box, VStack, Divider, useToast, Heading, Alert, Tag, Button, HStack, Modal, ModalOverlay, ModalContent, ModalBody, Select, useDisclosure } 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"
const UserProfilePage = () => {
const { isOpen, onOpen, onClose } = useDisclosure()
const [currentMember,setCurrentMember]:[User,any] = useState(null)
const [org, setOrg]:[User,any] = useState(null)
const [users, setUsers]: [User[], any] = useState(null)
const [secret, setSecret] = useState('')
const router = useRouter()
useEffect(() => {
if (router.query.org_id) {
getMembers()
requestApi.get(`/org/secret/${router.query.org_id}`).then(res => setSecret(res.data))
requestApi.get(`/user/info/${router.query.org_id}`).then(res => setOrg(res.data))
}
}, [router.query.org_id])
const getMembers = async () => {
const res = await requestApi.get(`/org/members/${router.query.org_id}`)
setUsers(res.data)
}
const toast = useToast()
const generateSecret = async () => {
const res = await requestApi.post(`/org/secret/${router.query.org_id}`)
toast({
description: "生成secret成功",
status: "success",
duration: 2000,
isClosable: true,
})
setSecret(res.data)
}
const onEdit = (member) => {
setCurrentMember(member)
onOpen()
}
const onChangeRole = e => {
const member = cloneDeep(currentMember)
member.role = e.currentTarget.value;
setCurrentMember(member)
}
const onSumitMember = async () => {
await requestApi.post(`/org/member/role`,{orgID:org.id, memberID: currentMember.id, role: currentMember.role})
setCurrentMember(null)
onClose()
getMembers()
}
return (
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={orgSettingLinks(router.query.org_id)} width={["120px", "120px", "250px", "250px"]} height="fit-content" title={`管理${org?.nickname}`} />
<Box ml={[1,1,4,4]} width="100%">
<Card>
<Heading size="sm">Grow the org</Heading>
<Text mt="2">Invite teammates by sending them the secret and the following instructions:</Text>
<VStack alignItems="left" mt="3" className="bordered" p="4">
<Text>1. Sign in</Text>
<Text>2. Navigate to {config.uiDomain}/settings/orgs</Text>
<Text>3. Paste the secret code below and click Join Organization</Text>
<Tag wordBreak="break-word">{secret}</Tag>
</VStack>
<HStack mt="4">
<Button variant="outline" onClick={generateSecret} _focus={null}>Generate new secret</Button>
<Text color="red.500">You should rotate this regularly!</Text>
</HStack>
</Card>
<Card mt="3">
{users &&
<VStack alignItems="left" width="100%">
{
users.map(u => <OrgMember user={u} key={u.id} onEdit={onEdit}/>)
}
</VStack>}
</Card>
</Box>
</Box>
</PageContainer>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
{currentMember && <ModalContent p="3">
<ModalBody mb="2">
<Text>Change role</Text>
<Select value={currentMember.role} _focus={null} mt="3" onChange={onChangeRole}>
<option value={Role.NORMAL}>{Role.NORMAL}</option>
<option value={Role.ADMIN}>{Role.ADMIN}</option>
</Select>
<Button colorScheme="teal" mt="3" onClick={onSumitMember}>Submit</Button>
</ModalBody>
</ModalContent>}
</Modal>
</>
)
}
export default UserProfilePage

@ -14,15 +14,14 @@ var validator = require('validator');
const UserProfilePage = () => {
const [user, setUser] = useState(null)
const [skills, setSkills] = useState([])
const [isLargerThan1280] = useMediaQuery("(min-width: 768px)")
const router = useRouter()
useEffect(() => {
if (router.query.id) {
requestApi.get(`/user/info/${router.query.id}`).then(res => setUser(res.data))
if (router.query.org_id) {
requestApi.get(`/user/info/${router.query.org_id}`).then(res => setUser(res.data))
}
}, [router.query.id])
}, [router.query.org_id])
const toast = useToast()
@ -103,7 +102,7 @@ const UserProfilePage = () => {
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={orgSettingLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="组织管理" />
<Sidebar routes={orgSettingLinks(router.query.org_id)} width={["120px", "120px", "250px", "250px"]} height="fit-content" title={`管理${user?.nickname}`} />
{user && <VStack alignItems="left" ml="4" width="100%">
<Formik
initialValues={user}

@ -15,11 +15,14 @@ import { useRouter } from "next/router"
import Link from "next/link"
import { ReserveUrls } from "src/data/reserve-urls"
const UserOrgsPage = () => {
const [orgs, setOrgs]:[Org[],any] = useState([])
const { isOpen, onOpen, onClose } = useDisclosure()
const { isOpen:isOpen1, onOpen:onOpen1, onClose:onClose1 } = useDisclosure()
const router = useRouter()
const stackBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark)
const [secret,setSecret] = useState('')
useEffect(() => {
getOrgs()
@ -41,9 +44,15 @@ const UserOrgsPage = () => {
onOpen()
}
const onJoinOrg = () => {
onOpen1()
}
const joinOrg = async () => {
await requestApi.post(`/org/join`,{secret: secret})
onClose1()
getOrgs()
}
return (
<>
<PageContainer>
@ -51,8 +60,11 @@ const UserOrgsPage = () => {
<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={onCreateOrg} _focus={null}></Button>
<Heading size="sm"></Heading>
<HStack>
<Button size="sm" onClick={onJoinOrg} _focus={null}></Button>
<Button colorScheme="teal" size="sm" onClick={onCreateOrg} _focus={null}></Button>
</HStack>
</Flex>
<VStack mt="3" divider={<StackDivider borderColor={stackBorderColor} />} alignItems="left">
@ -66,7 +78,7 @@ const UserOrgsPage = () => {
</HStack>
</Link>
<Button variant="outline" size="md" onClick={() => router.push(`${ReserveUrls.Settings}/org/profile?id=${o.id}`)}>Manage</Button>
{isAdmin(o.role) && <Button variant="outline" size="md" onClick={() => router.push(`${ReserveUrls.Settings}/org/profile/${o.id}`)}>Manage</Button>}
</Flex>)
}
</VStack>
@ -123,6 +135,18 @@ const UserOrgsPage = () => {
</ModalBody>
</ModalContent>}
</Modal>
<Modal isOpen={isOpen1} onClose={onClose1}>
<ModalOverlay />
{<ModalContent p="3">
<ModalBody mb="2">
<Text>Secret code</Text>
<Text fontSize=".9rem" layerStyle="textSecondary">Provided to you by an org admin</Text>
<Input _focus={null} mt="3" placeholder="..." value={secret} onChange={e => setSecret(e.currentTarget.value)}/>
<Button mt="6" colorScheme="teal" onClick={joinOrg}>Join Organization</Button>
</ModalBody>
</ModalContent>}
</Modal>
</>
)
}

@ -74,7 +74,7 @@ const UserProfilePage = () => {
)}
</Field>
<Field name="email" validate={validateEmail}>
<Field name="email" validate={v => validateEmail(v,false)}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.email && form.touched.email} >
<FormLabel></FormLabel>

@ -7,6 +7,7 @@ import (
"github.com/imdotdev/im.dev/server/internal/org"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
)
@ -88,3 +89,123 @@ func GetOrgMembers(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(users))
}
func GenOrgSecret(c *gin.Context) {
orgID := c.Param("id")
currentUser := user.CurrentUser(c)
if !org.IsOrgAdmin(currentUser.ID, orgID) {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
secret, err := user.GenSecret(orgID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(secret))
}
func GetOrgSecret(c *gin.Context) {
orgID := c.Param("id")
currentUser := user.CurrentUser(c)
if !org.IsOrgAdmin(currentUser.ID, orgID) {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
secret, err := user.GetSecret(orgID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(secret))
}
type JoinOrgReq struct {
Secret string `json:"secret"`
}
func JoinOrg(c *gin.Context) {
req := &JoinOrgReq{}
c.Bind(&req)
u := user.CurrentUser(c)
err := org.Join(req.Secret, u.ID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
type UpdateMemberReq struct {
OrgID string `json:"orgID"`
MemberID string `json:"memberID"`
Role models.RoleType `json:"role"`
}
func UpdateOrgMember(c *gin.Context) {
req := &UpdateMemberReq{}
c.Bind(&req)
if !req.Role.IsValid() {
c.JSON(http.StatusBadRequest, common.RespError(e.BadRequest))
return
}
if req.Role == models.ROLE_SUPER_ADMIN || req.Role == models.ROLE_EDITOR {
c.JSON(http.StatusBadRequest, common.RespError(e.BadRequest))
return
}
u := user.CurrentUser(c)
var role models.RoleType
err := db.Conn.QueryRow("SELECT role FROM org_member WHERE org_id=? and user_id=?", req.OrgID, u.ID).Scan(&role)
if err != nil {
logger.Warn("select role error", "error", err)
c.JSON(http.StatusBadRequest, common.RespError(e.BadRequest))
return
}
// 修改角色要求至少是管理员
if !role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError("你需要成为组织的管理员"))
return
}
// 目标角色修改为管理员,要求当前操作用户必须是超级管理员
if req.Role == models.ROLE_ADMIN {
if role != models.ROLE_SUPER_ADMIN {
c.JSON(http.StatusForbidden, common.RespError("你需要成为组织的超级管理员"))
return
}
}
// 若目标角色之前是管理员,那么必须是超级管理员才能对其修改
targetRole, err := org.GetMemberRole(req.OrgID, req.MemberID)
if err != nil {
c.JSON(http.StatusBadRequest, common.RespError(e.BadRequest))
return
}
if targetRole == models.ROLE_SUPER_ADMIN {
c.JSON(http.StatusForbidden, common.RespError("超级管理员不能被修改"))
return
}
if targetRole == models.ROLE_ADMIN {
if role != models.ROLE_SUPER_ADMIN {
c.JSON(http.StatusForbidden, common.RespError("你需要成为组织的超级管理员"))
return
}
}
err0 := org.UpdateMember(req.OrgID, req.MemberID, req.Role)
if err0 != nil {
c.JSON(err0.Status, common.RespError(err0.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}

@ -51,7 +51,7 @@ func GetUserSelf(c *gin.Context) {
func GetUser(c *gin.Context) {
username := c.Param("username")
userDetail, err := user.GetUserDetail("", username)
userDetail, err := user.GetUserDetail(username, username)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return

@ -10,6 +10,7 @@ import (
type Config struct {
AppName string `json:"appName"`
UIDomain string `json:"uiDomain"`
CommonMaxLen int `json:"commonMaxlen"`
Posts *PostsConfig `json:"posts"`
User *UserConfig `json:"user"`
@ -32,6 +33,7 @@ type UserConfig struct {
func GetConfig(c *gin.Context) {
conf := &Config{
AppName: config.Data.Common.AppName,
UIDomain: config.Data.UI.Domain,
CommonMaxLen: 255,
Posts: &PostsConfig{
TitleMaxLen: config.Data.Posts.TitleMaxLen,

@ -54,7 +54,7 @@ func Create(o *models.User, userID string) *e.Error {
return e.New(http.StatusInternalServerError, e.Internal)
}
_, err = tx.Exec("INSERT INTO org_member (org_id,user_id,role,created) VALUES (?,?,?,?)", o.ID, userID, models.ROLE_ADMIN, now)
_, err = tx.Exec("INSERT INTO org_member (org_id,user_id,role,created) VALUES (?,?,?,?)", o.ID, userID, models.ROLE_SUPER_ADMIN, now)
if err != nil {
logger.Warn("add org member error", "error", err)
tx.Rollback()
@ -67,7 +67,7 @@ func Create(o *models.User, userID string) *e.Error {
}
func GetMembers(user *models.User, orgID string) ([]*models.User, *e.Error) {
rows, err := db.Conn.Query("SELECT user_id from org_member where org_id=?", orgID)
rows, err := db.Conn.Query("SELECT user_id,role from org_member where org_id=?", orgID)
if err != nil {
logger.Warn("get org members error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
@ -75,16 +75,17 @@ func GetMembers(user *models.User, orgID string) ([]*models.User, *e.Error) {
users := make([]*models.User, 0)
for rows.Next() {
var id string
rows.Scan(&id)
var id, role string
rows.Scan(&id, &role)
u, ok := models.UsersMapCache[id]
if ok {
users = append(users, u)
if user != nil {
u.Followed = interaction.GetFollowed(u.ID, user.ID)
u.Follows = interaction.GetFollows(u.ID)
}
u.Role = models.RoleType(role)
users = append(users, u)
}
}
@ -126,3 +127,47 @@ func UserInOrg(userID string, orgID string) bool {
return true
}
func Join(secret string, userID string) *e.Error {
var orgID string
err := db.Conn.QueryRow("SELECT user_id FROM user_secret WHERE secret=?", secret).Scan(&orgID)
if err != nil {
if err == sql.ErrNoRows {
return e.New(http.StatusNotFound, "无效的secret")
}
logger.Warn("join org error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
if !models.IdExist(orgID) {
return e.New(http.StatusNotFound, "组织不存在")
}
_, err = db.Conn.Exec("INSERT INTO org_member (org_id,user_id,role,created) VALUES (?,?,?,?)", orgID, userID, models.ROLE_NORMAL, time.Now())
if err != nil {
logger.Warn("join org error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}
func UpdateMember(orgID, memberID string, role models.RoleType) *e.Error {
_, err := db.Conn.Exec("UPDATE org_member SET role=? WHERE org_id=? and user_id=?", role, orgID, memberID)
if err != nil {
logger.Warn("update org member error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}
func GetMemberRole(orgID string, memberID string) (models.RoleType, error) {
var role models.RoleType
err := db.Conn.QueryRow("SELECT role FROM org_member WHERE org_id=? and user_id=?", orgID, memberID).Scan(&role)
if err != nil {
return role, err
}
return role, nil
}

@ -108,7 +108,10 @@ func (s *Server) Start() error {
r.POST("/org/update", IsLogin(), api.UpdateOrg)
r.GET("/org/byUserID/:userID", api.GetOrgByUserID)
r.GET("/org/members/:id", api.GetOrgMembers)
r.POST("/org/secret/:id", IsLogin(), api.GenOrgSecret)
r.GET("/org/secret/:id", IsLogin(), api.GetOrgSecret)
r.POST("/org/join", IsLogin(), api.JoinOrg)
r.POST("/org/member/role", IsLogin(), api.UpdateOrgMember)
// admin apis
r.POST("/admin/user", IsLogin(), api.AdminSubmitUser)
r.GET("/admin/user/all", IsLogin(), api.AdminGetUsers)

@ -43,6 +43,15 @@ var sqlTables = map[string]string{
updated DATETIME
);`,
"user_secret": `CREATE TABLE IF NOT EXISTS user_secret (
user_id VARCHAR(255) primary key,
secret VARCHAR(255),
created DATETIME
);
CREATE UNIQUE INDEX IF NOT EXISTS us_secret
ON user_secret (secret);
`,
"sessions": `CREATE TABLE IF NOT EXISTS sessions (
sid VARCHAR(255) primary key,
user_id VARCHAR(255)

@ -0,0 +1,52 @@
package user
import (
"database/sql"
"net/http"
"time"
"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 GenSecret(userID string) (string, *e.Error) {
secret := utils.GenID(models.IDTypeSecret)
tx, err := db.Conn.Begin()
if err != nil {
logger.Warn("start transaction error", "error", err)
return "", e.New(http.StatusInternalServerError, e.Internal)
}
_, err = tx.Exec("DELETE FROM user_secret WHERE user_id=?", userID)
if err != nil {
logger.Warn("delete secret error", "error", err)
return "", e.New(http.StatusInternalServerError, e.Internal)
}
_, err = tx.Exec("INSERT INTO user_secret (user_id,secret,created) VALUES (?,?,?)", userID, secret, time.Now())
if err != nil {
logger.Warn("insert secret error", "error", err)
tx.Rollback()
return "", e.New(http.StatusInternalServerError, e.Internal)
}
tx.Commit()
return secret, nil
}
func GetSecret(userID string) (string, *e.Error) {
var secret string
err := db.Conn.QueryRow("SELECT secret from user_secret WHERE user_id=?", userID).Scan(&secret)
if err != nil {
if err == sql.ErrNoRows {
return "", e.New(http.StatusNotFound, "找不到对应的secret请重新生成")
}
logger.Warn("select secret error", "error", err)
return "", e.New(http.StatusInternalServerError, e.Internal)
}
return secret, nil
}

@ -24,10 +24,11 @@ type Session struct {
func Login(c *gin.Context) {
user := &models.User{}
err := user.Query("", config.Data.User.SuperAdminUsername, "")
c.Bind(&user)
err := user.Query("", "", user.Email)
if err != nil {
if err == sql.ErrNoRows {
c.String(http.StatusNotFound, "")
c.JSON(http.StatusNotFound, common.RespError("邮箱不存在"))
return
}
logger.Error("login error", "error", err)

@ -29,6 +29,10 @@ type Config struct {
BaseUrl string `yaml:"base_url"`
}
UI struct {
Domain string
}
Paths struct {
Data string
Logs string

@ -16,6 +16,7 @@ const (
IDTypeSeries = "5"
IDTypeBook = "6"
IDTypeOrg = "7"
IDTypeSecret = "s"
)
func GetIDType(id string) string {

@ -0,0 +1,53 @@
import React from "react"
import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Box, Avatar, VStack, propNames, Tag} from "@chakra-ui/react"
import moment from 'moment'
import { FaGithub } from "react-icons/fa"
import { useRouter } from "next/router"
import { User } from "src/types/user"
import { getUserName } from "utils/user"
import Follow from "components/interaction/follow"
import Highlighter from 'react-highlight-words';
import Count from "components/count"
type Props = PropsOf<typeof chakra.div> & {
user : User
highlight?: string
onEdit: any
}
export const OrgMember= ({user,highlight,onEdit}:Props) =>{
const router = useRouter()
return (
<Flex alignItems="center" justifyContent="space-between">
<HStack spacing="4" p="2">
<Image width="40px" src={user.avatar} onClick={() => router.push(`/${user.username}`)} cursor="pointer"/>
<VStack alignItems="left" spacing="1">
<HStack>
<Heading size="sm" onClick={() => router.push(`/${user.username}`)} cursor="pointer">
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={getUserName(user)}
searchWords={[highlight]}
/>
<Tag colorScheme="cyan" ml="2">{user.role}</Tag>
</Heading>
</HStack>
<Text layerStyle="textSecondary">@
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={user.username}
searchWords={[highlight]}
/> </Text>
</VStack>
</HStack>
<HStack>
<Text fontWeight="600" fontSize=".95rem"><Count count={user.follows??0}/> followers</Text>
<Button variant="outline" size="sm" onClick={() => onEdit(user)} _focus={null}>Edit</Button>
</HStack>
</Flex>
)
}
export default OrgMember

@ -1,5 +1,5 @@
import React from "react"
import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Box, Avatar, VStack, propNames} from "@chakra-ui/react"
import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Box, Avatar, VStack, propNames, Tag} from "@chakra-ui/react"
import moment from 'moment'
import { FaGithub } from "react-icons/fa"
import { useRouter } from "next/router"
@ -12,14 +12,16 @@ import Count from "components/count"
type Props = PropsOf<typeof chakra.div> & {
user : User
highlight?: string
displayFollow?: boolean
displayRole?:boolean
}
export const UserCard= ({user,highlight}:Props) =>{
export const UserCard= ({user,highlight,displayFollow=true,displayRole=false}:Props) =>{
const router = useRouter()
return (
<Flex alignItems="center" justifyContent="space-between">
<HStack spacing="4" p="2">
<Avatar src={user.avatar} onClick={() => router.push(`/${user.username}`)} cursor="pointer"/>
<Image width="40px" src={user.avatar} onClick={() => router.push(`/${user.username}`)} cursor="pointer"/>
<VStack alignItems="left" spacing="1">
<HStack>
<Heading size="sm" onClick={() => router.push(`/${user.username}`)} cursor="pointer">
@ -36,7 +38,7 @@ export const UserCard= ({user,highlight}:Props) =>{
searchWords={[highlight]}
/> </Text>
</HStack>
{user.tagline && <Text>
{user.tagline && <Text fontSize=".95rem">
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={user.tagline}
@ -46,8 +48,9 @@ export const UserCard= ({user,highlight}:Props) =>{
</VStack>
</HStack>
<HStack>
{displayRole && <Tag colorScheme="cyan">{user.role}</Tag>}
<Text fontWeight="600" fontSize=".9rem"><Count count={user.follows??0}/> followers</Text>
<Follow followed={user.followed} targetID={user.id} size="sm"/>
{displayFollow && <Follow followed={user.followed} targetID={user.id} size="sm"/>}
</HStack>
</Flex>

@ -92,26 +92,30 @@ export const settingLinks: Route[] = [{
disabled: false
},
{
title: '组织管理',
title: '组织列表',
path: `${ReserveUrls.Settings}/orgs`,
icon: <FaUserFriends />,
disabled: false
},
]
export const orgSettingLinks: Route[] = [{
title: '组织信息',
path: `${ReserveUrls.Settings}/org/profile`,
icon: <FaUserCircle />,
disabled: false
},
{
title: '成员管理',
path: `${ReserveUrls.Settings}/org/members`,
icon: <FaUserFriends />,
disabled: false
},
]
export function orgSettingLinks(orgID) {
return [{
title: '组织信息',
path: `${ReserveUrls.Settings}/org/profile/${orgID}`,
icon: <FaUserCircle />,
disabled: false
},
{
title: '成员管理',
path: `${ReserveUrls.Settings}/org/members/${orgID}`,
icon: <FaUserFriends />,
disabled: false
},
]
}

@ -54,7 +54,7 @@ export function validateNickname(value) {
return error
}
export async function validateEmail(value) {
export async function validateEmail(value,checkExist=true) {
let email = value?.trim()
if (!email) {
return "邮箱不能为空"
@ -70,9 +70,11 @@ export async function validateEmail(value) {
}
}
const res = await requestApi.get(`/user/email/exist/${value}`)
if (res.data) {
return `The email '${value}' is already taken.`
if (checkExist) {
const res = await requestApi.get(`/user/email/exist/${value}`)
if (res.data) {
return `The email '${value}' is already taken.`
}
}
}

Loading…
Cancel
Save