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
+
+
+
+
+ Next
+ > :
+ <>
+ (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}
+
+
+
+ )
+ }
+
+
+ Finish
+ >
+ }
+
+
+ )
+}
+
+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 ?
- }>Following
+ :null}>Following
:
- } onClick={follow} _focus={null}>Follow
+ : null} onClick={follow} _focus={null}>Follow
}
>
)