add email login support

pull/52/head
sunface 4 years ago
parent 0d9dff4d27
commit d4bb937778

@ -0,0 +1,44 @@
import { useRouter } from "next/router"
import React, { useEffect, useState } from "react"
import { requestApi } from "utils/axios/request"
import { saveToken } from "utils/axios/getToken"
import storage from "utils/localStorage"
const LoginCodePage = () => {
const router = useRouter()
const code = router.query.code
useEffect(() => {
if (code) {
login(code)
}
}, [code])
const login = async (code) => {
const res = await requestApi.post("/user/login/code", { code: code })
if (res.data) {
//已经注册过
saveToken(res.data.token)
storage.set('session', res.data)
const oldPage = storage.get('current-page')
if (oldPage) {
storage.remove('current-page')
router.push(oldPage)
} else {
router.push('/')
}
} else {
// 进入注册流程
router.push(`/login/onboard?code=${code}`)
}
}
return (
<>
</>
)
}
export default LoginCodePage

@ -0,0 +1,124 @@
import { useRouter } from "next/router"
import React, { useEffect, useState } from "react"
import { requestApi } from "utils/axios/request"
import { saveToken } from "utils/axios/getToken"
import storage from "utils/localStorage"
import { Avatar, Box, Button, Center, Flex, Heading, HStack, Input, Text, useToast, VStack, Wrap } from "@chakra-ui/react"
import Card from "components/card"
import { config } from "configs/config"
import Link from "next/link"
import { ReserveUrls } from "src/data/reserve-urls"
import { Tag } from "src/types/tag"
import Follow from "components/interaction/follow"
const OnboardPage = () => {
const [step,setStep] = useState(1)
const [email,setEmail] = useState('')
const [nickname,setNickname] = useState('')
const [username,setUsername] = useState('')
const [tags,setTags]:[Tag[],any] = useState([])
const router = useRouter()
const toast = useToast()
const code = router.query.code
useEffect(() => {
if (code) {
getEmailByCode(code)
}
}, [code])
const getEmailByCode = async (code) => {
const res = await requestApi.get(`/user/email/byCode?code=${code}`)
if (!res.data) {
toast({
description: "code不存在或者已失效",
status: "error",
duration: 2000,
isClosable: true,
})
setTimeout(() => router.push('/login'),2000)
}
setEmail(res.data)
}
const register = async () => {
if (nickname === "" || username === "") {
toast({
description: "nickname or username can't be empty",
status: "error",
duration: 2000,
isClosable: true,
})
return
}
const res = await requestApi.post("/user/register", { code: code, nickname: nickname,username: username })
saveToken(res.data.token)
storage.set('session', res.data)
setStep(2)
const res1 = await requestApi.get(`/tag/all`)
setTags(res1.data)
}
const finish = async () => {
const oldPage = storage.get('current-page')
if (oldPage) {
storage.remove('current-page')
router.push(oldPage)
} else {
router.push('/')
}
}
return (
<Center h="100vh" mt="-8">
<Card p="6" width="600px">
{step === 1 ? <>
<Text layerStyle="textSecondary" fontWeight="bold">CREATE YOUR ACCOUNT</Text>
<Heading size="md" mt="2">🤘 Let's start your {config.appName} journey</Heading>
<VStack alignItems="left" mt="8" spacing="4">
<HStack>
<Text width="150px">Nick name</Text>
<Input value={nickname} onChange={e => setNickname(e.currentTarget.value)} size="lg" width="auto" placeholder="enter your nick name" _focus={null}></Input>
</HStack>
<HStack>
<Text width="150px">User name</Text>
<Input value={username} onChange={e => setUsername(e.currentTarget.value)} size="lg" width="auto" placeholder="user name is unique, and cant be changed anymore" _focus={null}></Input>
</HStack>
<HStack>
<Text width="150px">Email address</Text>
<Input size="lg" width="auto" value={email} _focus={null} disabled></Input>
</HStack>
</VStack>
<Button colorScheme="teal" size="lg" mt="6" float="right" onClick={register}>Next</Button>
</> :
<>
<Text layerStyle="textSecondary" fontWeight="bold">(OPTIONAL) PERSONALIZE YOUR HASHNODE FEED</Text>
<Heading size="md" mt="2">Follow technologies you care about.</Heading>
<Text mt="2" layerStyle="textSecondary" fontSize=".9rem">Hashnode is a platform for independent bloggers. By following the right tags you can personalize your feed and discover content you care about.</Text>
<Wrap mt="4" p="1">
{
tags.map(tag =>
<HStack key={tag.id} mr="4" mb="4">
<HStack spacing="1" mr="3" cursor="pointer" width="120px">
<Avatar src={tag.icon} size="sm" />
<Text>{tag.title}</Text>
</HStack>
<Follow targetID={tag.id} followed={false} buttonIcon={false}/>
</HStack>)
}
</Wrap>
<Button colorScheme="teal" size="lg" mt="6" float="right" onClick={finish}>Finish</Button>
</>
}
</Card>
</Center>
)
}
export default OnboardPage

@ -174,3 +174,27 @@ func UserEmailExist(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(exist))
}
func GetUserEmailByCode(c *gin.Context) {
code := c.Query("code")
email, err := user.GetEmailByCode(code)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(email))
}
type RegisterReq struct {
Code string `json:"code"`
Nickname string `json:"nickname"`
Username string `json:"username"`
}
func UserRegister(c *gin.Context) {
req := &RegisterReq{}
c.Bind(&req)
user.Register(c, req.Code, req.Nickname, req.Username)
}

@ -108,13 +108,15 @@ func (s *Server) Start() error {
r.GET("/user/session", api.GetSession)
r.POST("/user/login", user.Login)
r.POST("/user/login/email", user.LoginEmail)
r.POST("/user/login/code", user.LoginCode)
r.POST("/user/logout", user.Logout)
r.POST("/user/navbar", IsLogin(), api.SubmitUserNavbar)
r.GET("/user/navbars/:userID", api.GetUserNavbars)
r.DELETE("/user/navbar/:id", IsLogin(), api.DeleteUserNavbar)
r.GET("/user/name/exist/:name", api.UserNameExist)
r.GET("/user/email/exist/:email", api.UserEmailExist)
r.GET("/user/email/byCode", api.GetUserEmailByCode)
r.POST("/user/register", api.UserRegister)
// interaction apis
r.POST("/interaction/like/:id", IsLogin(), api.Like)
r.POST("/interaction/follow/:id", IsLogin(), api.Follow)

@ -280,4 +280,9 @@ var sqlTables = map[string]string{
data TEXT,
updated DATETIME
);`,
"mail_code": `CREATE TABLE IF NOT EXISTS mail_code (
code VARCHAR(255) PRIMARY KEY,
mail VARCHAR(255),
created DATETIME
);`,
}

@ -15,6 +15,7 @@ import (
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/log"
"github.com/imdotdev/im.dev/server/pkg/models"
"github.com/imdotdev/im.dev/server/pkg/utils"
"github.com/matcornic/hermes/v2"
)
@ -29,6 +30,7 @@ type Session struct {
func Login(c *gin.Context) {
user := &models.User{}
c.Bind(&user)
err := user.Query("", "", user.Email)
if err != nil {
if err == sql.ErrNoRows {
@ -40,6 +42,10 @@ func Login(c *gin.Context) {
return
}
login(user, c)
}
func login(user *models.User, c *gin.Context) {
// delete old session
token := getToken(c)
deleteSession(token)
@ -51,7 +57,7 @@ func Login(c *gin.Context) {
CreateTime: time.Now(),
}
err = storeSession(session)
err := storeSession(session)
if err != nil {
c.JSON(500, common.RespInternalError())
return
@ -65,6 +71,52 @@ func Login(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(session))
}
type CodeReq struct {
Code string `json:"code"`
}
func LoginCode(c *gin.Context) {
req := &CodeReq{}
c.Bind(&req)
// 通过code查询邮箱地址
var created time.Time
var email string
err := db.Conn.QueryRow("SELECT mail,created FROM mail_code WHERE code=?", req.Code).Scan(&email, &created)
if err != nil && err != sql.ErrNoRows {
c.JSON(http.StatusInternalServerError, common.RespInternalError())
return
}
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, common.RespError("code不合法"))
return
}
if time.Now().Sub(created).Hours() > 6 {
c.JSON(http.StatusBadRequest, common.RespError("code已过期"))
db.Conn.Exec("DELETE FROM mail_code WHERE code=?", req.Code)
return
}
user := &models.User{
Email: email,
}
err = user.Query("", "", user.Email)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusOK, common.RespSuccess(nil))
return
}
logger.Error("login error", "error", err)
c.JSON(http.StatusInternalServerError, common.RespInternalError())
return
}
login(user, c)
}
func LoginEmail(c *gin.Context) {
user := &models.User{}
c.Bind(&user)
@ -74,7 +126,18 @@ func LoginEmail(c *gin.Context) {
return
}
fmt.Println(user.Email)
code := utils.GenID("code-")
_, err := db.Conn.Exec("DELETE FROM mail_code WHERE mail=?", user.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, common.RespInternalError())
return
}
_, err = db.Conn.Exec("INSERT INTO mail_code (code,mail,created) VALUES (?,?,?)", code, user.Email, time.Now())
if err != nil {
c.JSON(http.StatusInternalServerError, common.RespInternalError())
return
}
e := hermes.Email{
Body: hermes.Body{
@ -89,7 +152,7 @@ func LoginEmail(c *gin.Context) {
Button: hermes.Button{
Color: "#22BC66", // Optional action button color
Text: "Confirm your account",
Link: config.Data.UI.Domain,
Link: fmt.Sprintf("%s/login/%s", config.Data.UI.Domain, code),
},
},
},

@ -6,9 +6,11 @@ import (
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/interaction"
"github.com/imdotdev/im.dev/server/internal/org"
"github.com/imdotdev/im.dev/server/internal/tags"
"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"
@ -163,6 +165,21 @@ func EmailExist(email string) (bool, *e.Error) {
return true, nil
}
func GetEmailByCode(code string) (string, *e.Error) {
var email string
err := db.Conn.QueryRow("SELECT mail FROM mail_code WHERE code=?", code).Scan(&email)
if err != nil && err != sql.ErrNoRows {
logger.Warn("check email exist error", "error", err)
return "", e.New(http.StatusInternalServerError, e.Internal)
}
if err == sql.ErrNoRows {
return "", nil
}
return email, nil
}
func SubmitUser(user *models.User) *e.Error {
if user.Nickname == "" {
user.Nickname = "New user"
@ -171,6 +188,15 @@ func SubmitUser(user *models.User) *e.Error {
var err error
now := time.Now()
if user.ID == "" {
nameExist, err0 := NameExist(user.Username)
if err0 != nil {
return e.New(err0.Status, err0.Message)
}
if nameExist {
return e.New(http.StatusConflict, "username已存在")
}
// create user
emailExist, err0 := EmailExist(user.Email)
if err0 != nil {
@ -196,3 +222,23 @@ func SubmitUser(user *models.User) *e.Error {
return nil
}
func Register(c *gin.Context, code string, nickname string, username string) {
email, err0 := GetEmailByCode(code)
if err0 != nil {
c.JSON(err0.Status, common.RespError(err0.Message))
return
}
user := &models.User{Nickname: nickname, Username: username, Email: email}
err0 = SubmitUser(user)
if err0 != nil {
c.JSON(err0.Status, common.RespError(err0.Message))
return
}
user.Query("", "", user.Email)
// 从mail_code中删除code
db.Conn.Exec("DELETE FROM mail_code WHERE code=?", code)
login(user, c)
}

@ -7,9 +7,10 @@ interface Props {
targetID: string
followed: boolean
size?: string
buttonIcon?: boolean
}
const Follow = (props: Props) => {
const {size="md"} =props
const {size="md",buttonIcon=true} =props
const [followed, setFollowed] = useState(props.followed)
const follow = async () => {
@ -20,9 +21,9 @@ const Follow = (props: Props) => {
return (
<>
{followed ?
<Button size={size} colorScheme="teal" onClick={follow} _focus={null} leftIcon={<FaCheck />}>Following</Button>
<Button size={size} colorScheme="teal" onClick={follow} _focus={null} leftIcon={buttonIcon?<FaCheck />:null}>Following</Button>
:
<Button size={size} colorScheme="teal" variant="outline" leftIcon={<FaPlus />} onClick={follow} _focus={null}>Follow</Button>
<Button size={size} colorScheme="teal" variant="outline" leftIcon={buttonIcon ? <FaPlus /> : null} onClick={follow} _focus={null}>Follow</Button>
}
</>
)

Loading…
Cancel
Save