diff --git a/config.yaml b/config.yaml index 8c5cf39c..5219d0e4 100644 --- a/config.yaml +++ b/config.yaml @@ -35,4 +35,9 @@ posts: title_max_len: 128 brief_max_len: 128 # whether allow writing posts - writing_enabled: true \ No newline at end of file + writing_enabled: true + +#################################### SMTP ############################## +smtp: + from_address: "hello@im.dev" + from_name: "Im'dev" \ No newline at end of file diff --git a/go.mod b/go.mod index 32d3deae..a3e93eee 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gosimple/slug v1.9.0 github.com/grafana/grafana v5.4.5+incompatible github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/lithammer/shortuuid/v3 v3.0.5 github.com/mattn/go-sqlite3 v1.14.6 github.com/spf13/cobra v1.1.1 diff --git a/go.sum b/go.sum index 8af860cd..ae82d86c 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,8 @@ github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac/go.mod h1:cO github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= diff --git a/pages/login.tsx b/pages/login.tsx index 4bef5aa7..92b40087 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -17,10 +17,11 @@ import { ModalBody, useDisclosure, Input, - useToast + useToast, + Heading } from "@chakra-ui/react" import Logo from "components/logo" -import { FaEnvelope, FaGithub } from "react-icons/fa" +import { FaEnvelope, FaGithub, FaPlane, FaRegPaperPlane } from "react-icons/fa" import { requestApi } from "utils/axios/request" import { saveToken } from "utils/axios/getToken" import storage from "utils/localStorage" @@ -32,9 +33,10 @@ const LoginPage = () => { const { isOpen, onOpen, onClose } = useDisclosure() const toast = useToast() const router = useRouter() - const [email,setEmail] = useState('') - const login = async (email:string) => { - const res = await requestApi.post("/user/login",{email: email}) + const [email, setEmail] = useState('') + const [emailLogined, setEmailLogined] = useState(false) + 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') @@ -47,7 +49,7 @@ const LoginPage = () => { } const onEmailLogin = async () => { - const err = await validateEmail(email,false) + const err = await validateEmail(email, false) if (err) { toast({ description: err, @@ -55,10 +57,13 @@ const LoginPage = () => { duration: 2000, isClosable: true, }) - return + return } - login(email) + await requestApi.post("/user/login/email", { email: email }) + onClose() + setEmailLogined(true) + // login(email) } return ( @@ -67,32 +72,40 @@ const LoginPage = () => { 欢迎加入im.dev,一起打造全世界最好的开发者社区 - - - - 从世界各地精选最优秀的内容 - - - - 丰富的功能特性等待你的探索 - - - - 充分展示自我并获得猎头关注 - - - - - OR - } onClick={onOpen}/> - - 如果继续,则表示你同意im.dev的服务条款和隐私政策 - {/* */} + { + emailLogined ? + + + Check your inbox for a secure link to sign in. + : + <> + + + + 从世界各地精选最优秀的内容 + + + + 丰富的功能特性等待你的探索 + + + + 充分展示自我并获得猎头关注 + + + + + OR + } onClick={onOpen} /> + + 如果继续,则表示你同意im.dev的服务条款和隐私政策 + + } - + Sign in using a secure link diff --git a/pages/notifications.tsx b/pages/notifications.tsx index 663ab48b..1f491a56 100644 --- a/pages/notifications.tsx +++ b/pages/notifications.tsx @@ -2,11 +2,6 @@ import { Heading, HStack, Text, VStack, Divider, Wrap, - Image, - useColorModeValue, - Box, - StackDivider, - Tag } from "@chakra-ui/react" import SEO from "components/seo" import siteConfig from "configs/site-config" @@ -14,19 +9,8 @@ import { import React, { useEffect, useState } from "react" import { IndexSidebar } from 'pages/index' import Card from "components/card" - import { config } from "configs/config" import { requestApi } from "utils/axios/request" -import { Story } from "src/types/story" -import { find } from "lodash" -import Empty from "components/empty" -import StoryCard from "components/story/story-card" -import { FaBell } from "react-icons/fa" import { getSvgIcon } from "components/svg-icon" -import { Notification } from "src/types/notification" -import { getUserName } from "utils/user" -import moment from 'moment' -import userCustomTheme from "theme/user-custom" -import Link from "next/link" import Notifications from "components/notifications" const filters = [ @@ -34,7 +18,7 @@ const filters = [ {icon: 'comments',label:'Comments',type: 1}, {icon: 'favorites',label:'Likes', type: 2}, {icon: 'follow',label:'Follows', type: 5}, - {icon: 'at',label: 'Mentions', type: 3}, + // {icon: 'at',label: 'Mentions', type: 3}, {icon: 'post',label: 'Stories', type: 4}, ] diff --git a/server/internal/email/email.go b/server/internal/email/email.go new file mode 100644 index 00000000..32bfcb42 --- /dev/null +++ b/server/internal/email/email.go @@ -0,0 +1,59 @@ +package email + +import ( + "fmt" + "html/template" + "net/smtp" + "strings" + + "github.com/jordan-wright/email" +) + +var MailTemplates *template.Template + +func init() { + MailTemplates = template.New("im.dev") + tmpl, err := template.ParseGlob("./server/internal/email/templates/*.tmpl") + if err != nil { + panic(err) + } + + MailTemplates = tmpl +} + +type EmailContent struct { + To []string + Template string + Subject string + Data map[string]interface{} +} + +type EmailMessage struct { + To []string + From string + Subject string + Body string +} + +func Send(msg *EmailMessage) error { + e := email.NewEmail() + + for _, to := range msg.To { + e.From = msg.From + e.To = []string{to} + e.Subject = msg.Subject + e.HTML = []byte(msg.Body) + + fmt.Println(e.To) + r := strings.Split(to, "@") + s := fmt.Sprintf("smtp.%s:25", r[1]) + fmt.Println("smtp:", s) + err := e.Send(s, smtp.PlainAuth("", "61087682@qq.com", "nybusxktxfyycahh", "smtp.qq.com")) + + if err != nil { + return err + } + } + + return nil +} diff --git a/server/internal/email/templates/base.tmpl b/server/internal/email/templates/base.tmpl new file mode 100644 index 00000000..712d714d --- /dev/null +++ b/server/internal/email/templates/base.tmpl @@ -0,0 +1,991 @@ + + + + + + + + + + + + + + + +
+
+ + + + + + + +
+ {{block "content" . }}{{end}} + +
+
+
+ + diff --git a/server/internal/email/templates/login.tmpl b/server/internal/email/templates/login.tmpl new file mode 100644 index 00000000..bcd8cb25 --- /dev/null +++ b/server/internal/email/templates/login.tmpl @@ -0,0 +1,70 @@ +{{template "base.tmpl" .}} +​ +{{define "content"}} + + + + +
+ + + + +
+

{{.Title}}

+
+
+ + + + + +
+ + + + +
+

Use the following link to sign up or automatically login to Hashnode, be careful who you give it to.

+
+
+ + + + + + + +
+ + + + +
+ + + + +
+ Click here to sign in +
+
+
+ + + + + + +
+ + + + +
+

Or copy this url into your browser

+

{{.URL}}

+
+
+ +{{end}} \ No newline at end of file diff --git a/server/internal/notification/notification.go b/server/internal/notification/notification.go index 46f0cc07..6003598c 100644 --- a/server/internal/notification/notification.go +++ b/server/internal/notification/notification.go @@ -91,6 +91,10 @@ func Query(user *models.User, tp int, page int) ([]*models.Notification, *e.Erro } case models.NotificationFollow: no.Title = " started following you" + case models.NotificationPublish: + no.Title = " published a new story" + no.SubTitle = models.GetStoryTitle(noID) + no.StoryID = noID } nos = append(nos, no) } diff --git a/server/internal/server.go b/server/internal/server.go index 3de273cb..a98041a5 100644 --- a/server/internal/server.go +++ b/server/internal/server.go @@ -96,6 +96,7 @@ func (s *Server) Start() error { r.GET("/user/posts/:userID", api.GetUserPosts) r.GET("/user/session", api.GetSession) r.POST("/user/login", user.Login) + r.POST("/user/login/email", user.LoginEmail) r.POST("/user/logout", user.Logout) r.POST("/user/navbar", IsLogin(), api.SubmitUserNavbar) r.GET("/user/navbars/:userID", api.GetUserNavbars) diff --git a/server/internal/story/post.go b/server/internal/story/post.go index 564831cf..44f32e03 100644 --- a/server/internal/story/post.go +++ b/server/internal/story/post.go @@ -108,6 +108,7 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) { logger.Warn("submit post error", "error", err) return nil, e.New(http.StatusInternalServerError, e.Internal) } + } else { post.Updated = now // 只有创建者自己才能更新内容 @@ -121,6 +122,24 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) { // 首次发布,需要更新创建时间 _, err = db.Conn.Exec("UPDATE story SET owner=?, slug=?, title=?, md=?, url=?, cover=?, brief=?,created=?,updated=?,status=? WHERE id=?", post.OwnerID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, post.Created, post.Updated, models.StatusPublished, post.ID) + + // send notification to creator followers and org followers + followers, err1 := interaction.GetFollowerIDs(post.CreatorID) + if err1 == nil { + for _, f := range followers { + notification.Send(f, "", models.NotificationPublish, post.ID, post.CreatorID) + } + } + + if post.OwnerID != "" { + followers, err1 = interaction.GetFollowerIDs(post.OwnerID) + if err1 == nil { + for _, f := range followers { + notification.Send("", f, models.NotificationPublish, post.ID, post.CreatorID) + } + } + } + } else { _, err = db.Conn.Exec("UPDATE story SET owner=?, slug=?, title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?", post.OwnerID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, post.Updated, post.ID) @@ -142,23 +161,6 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) { likes := interaction.GetLikes(post.ID) top.Update(post.ID, likes) - // send notification to creator followers and org followers - followers, err1 := interaction.GetFollowerIDs(post.CreatorID) - if err1 == nil { - for _, f := range followers { - notification.Send(f, "", models.NotificationFollow, post.ID, post.CreatorID) - } - } - - if post.OwnerID != "" { - followers, err1 = interaction.GetFollowerIDs(post.OwnerID) - if err1 == nil { - for _, f := range followers { - notification.Send("", f, models.NotificationFollow, post.ID, post.CreatorID) - } - } - } - return map[string]string{ "username": user.Username, "id": post.ID, diff --git a/server/internal/user/session.go b/server/internal/user/session.go index 279f766c..69fc04e1 100644 --- a/server/internal/user/session.go +++ b/server/internal/user/session.go @@ -1,12 +1,16 @@ package user import ( + "bytes" "database/sql" + "fmt" "net/http" "strconv" "time" + "github.com/asaskevich/govalidator" "github.com/gin-gonic/gin" + "github.com/imdotdev/im.dev/server/internal/email" "github.com/imdotdev/im.dev/server/pkg/common" "github.com/imdotdev/im.dev/server/pkg/config" "github.com/imdotdev/im.dev/server/pkg/db" @@ -61,6 +65,77 @@ func Login(c *gin.Context) { c.JSON(http.StatusOK, common.RespSuccess(session)) } +func LoginEmail(c *gin.Context) { + user := &models.User{} + c.Bind(&user) + + if !govalidator.IsEmail(user.Email) { + c.JSON(http.StatusBadRequest, common.RespError("邮件格式不合法")) + return + } + + fmt.Println(user.Email) + + var buffer bytes.Buffer + err := email.MailTemplates.ExecuteTemplate(&buffer, "login.tmpl", map[string]string{ + "Title": "Im.dev", + "URL": "https://im.dev/login", + }) + if err != nil { + logger.Warn("login with email error", "error", err, "email", user.Email) + c.JSON(http.StatusInternalServerError, common.RespInternalError()) + return + } + + emailMsg := &email.EmailMessage{ + To: []string{user.Email}, + From: fmt.Sprintf("%s <%s>", config.Data.SMTP.FromName, "61087682@qq.com"), + Subject: fmt.Sprintf("Sign in to %s", config.Data.Common.AppName), + Body: buffer.String(), + } + + err = email.Send(emailMsg) + if err != nil { + logger.Warn("send login email error", "error", err) + c.JSON(http.StatusBadRequest, common.RespError(err.Error())) + return + } + // err := user.Query("", "", user.Email) + // if err != nil { + // if err == sql.ErrNoRows { + // c.JSON(http.StatusNotFound, common.RespError("邮箱不存在")) + // return + // } + // logger.Error("login error", "error", err) + // c.JSON(http.StatusInternalServerError, common.RespInternalError()) + // return + // } + + // // delete old session + // token := getToken(c) + // deleteSession(token) + + // token = strconv.FormatInt(time.Now().UnixNano(), 10) + // session := &Session{ + // Token: token, + // User: user, + // CreateTime: time.Now(), + // } + + // err = storeSession(session) + // if err != nil { + // c.JSON(500, common.RespInternalError()) + // return + // } + + // _, err = db.Conn.Exec(`UPDATE user SET last_seen_at=? WHERE id=?`, time.Now(), user.ID) + // if err != nil { + // logger.Warn("set last login date error", "error", err) + // } + + c.JSON(http.StatusOK, common.RespSuccess(nil)) +} + // Logout ... func Logout(c *gin.Context) { token := getToken(c) diff --git a/server/pkg/config/config.go b/server/pkg/config/config.go index 3f498db1..38dcfbf0 100644 --- a/server/pkg/config/config.go +++ b/server/pkg/config/config.go @@ -43,6 +43,11 @@ type Config struct { BriefMaxLen int `yaml:"brief_max_len"` WritingEnabled bool `yaml:"writing_enabled"` } + + SMTP struct { + FromAddr string `yaml:"from_address"` + FromName string `yaml:"from_name"` + } } // Data ...