From d4bb93777896eb06fd4c88da7f4e111dbadf5f55 Mon Sep 17 00:00:00 2001 From: sunface Date: Thu, 8 Apr 2021 18:29:21 +0800 Subject: [PATCH] add email login support --- pages/login/[code].tsx | 44 +++++++++ pages/{login.tsx => login/index.tsx} | 0 pages/login/onboard.tsx | 124 ++++++++++++++++++++++++++ server/internal/api/user.go | 24 +++++ server/internal/server.go | 4 +- server/internal/storage/sql_tables.go | 5 ++ server/internal/user/session.go | 69 +++++++++++++- server/internal/user/user.go | 46 ++++++++++ src/components/interaction/follow.tsx | 7 +- 9 files changed, 316 insertions(+), 7 deletions(-) create mode 100644 pages/login/[code].tsx rename pages/{login.tsx => login/index.tsx} (100%) create mode 100644 pages/login/onboard.tsx diff --git a/pages/login/[code].tsx b/pages/login/[code].tsx new file mode 100644 index 00000000..a8b848df --- /dev/null +++ b/pages/login/[code].tsx @@ -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 + + diff --git a/pages/login.tsx b/pages/login/index.tsx similarity index 100% rename from pages/login.tsx rename to pages/login/index.tsx diff --git a/pages/login/onboard.tsx b/pages/login/onboard.tsx new file mode 100644 index 00000000..0f52e6c1 --- /dev/null +++ b/pages/login/onboard.tsx @@ -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 ( +
+ + {step === 1 ? <> + CREATE YOUR ACCOUNT + 🤘 Let's start your {config.appName} journey + + + + Nick name + setNickname(e.currentTarget.value)} size="lg" width="auto" placeholder="enter your nick name" _focus={null}> + + + User name + setUsername(e.currentTarget.value)} size="lg" width="auto" placeholder="user name is unique, and cant be changed anymore" _focus={null}> + + + Email address + + + + + + : + <> + (OPTIONAL) PERSONALIZE YOUR HASHNODE FEED + Follow technologies you care about. + Hashnode is a platform for independent bloggers. By following the right tags you can personalize your feed and discover content you care about. + + { + tags.map(tag => + + + + {tag.title} + + + + ) + } + + + + + } + +
+ ) +} + +export default OnboardPage + + diff --git a/server/internal/api/user.go b/server/internal/api/user.go index c829e36f..cdbd269e 100644 --- a/server/internal/api/user.go +++ b/server/internal/api/user.go @@ -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) +} diff --git a/server/internal/server.go b/server/internal/server.go index bdc1cf11..07cad3f4 100644 --- a/server/internal/server.go +++ b/server/internal/server.go @@ -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) diff --git a/server/internal/storage/sql_tables.go b/server/internal/storage/sql_tables.go index 07738e3c..53ee54a0 100644 --- a/server/internal/storage/sql_tables.go +++ b/server/internal/storage/sql_tables.go @@ -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 + );`, } diff --git a/server/internal/user/session.go b/server/internal/user/session.go index 47b3cd54..5b0f9534 100644 --- a/server/internal/user/session.go +++ b/server/internal/user/session.go @@ -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), }, }, }, diff --git a/server/internal/user/user.go b/server/internal/user/user.go index ccf3a91f..1f693ed1 100644 --- a/server/internal/user/user.go +++ b/server/internal/user/user.go @@ -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) +} diff --git a/src/components/interaction/follow.tsx b/src/components/interaction/follow.tsx index baf893d9..416c1141 100644 --- a/src/components/interaction/follow.tsx +++ b/src/components/interaction/follow.tsx @@ -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 ? - + : - + } )