diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index fa31653..42b3d34 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -24,9 +24,9 @@ var ( Ei *Cache // regenerate pages chan - pagesCh = make(chan string, 2) - pageSeries = "series-md" - pageArchive = "archive-md" + PagesCh = make(chan string, 2) + PageSeries = "series-md" + PageArchive = "archive-md" ) func init() { @@ -141,6 +141,11 @@ func (c *Cache) PageArticles(page int, pageSize int) (prev, return } +// PageArticlesBE 后台文章分页 +// func (c *Cache) PageArticleBE(se int, kw string, draft, del bool, p, n int)(max int, artcs []*model.Article){ +// +// } + // loadOrInit 读取数据或初始化 func (c *Cache) loadOrInit() error { blogapp := config.Conf.BlogApp @@ -218,8 +223,8 @@ func (c *Cache) loadOrInit() error { } Ei.Articles = articles // 重建专题与归档 - pagesCh <- pageSeries - pagesCh <- pageArchive + PagesCh <- PageSeries + PagesCh <- PageArchive return nil } @@ -239,7 +244,7 @@ func (c *Cache) rebuildArticle(article *model.Article, needSort bool) { c.Series[i].Articles = append(c.Series[i].Articles, article) if needSort { sort.Sort(c.Series[i].Articles) - pagesCh <- pageSeries // 重建专题 + PagesCh <- PageSeries // 重建专题 } } } @@ -251,7 +256,7 @@ func (c *Cache) rebuildArticle(article *model.Article, needSort bool) { } if needSort { sort.Sort(c.Archives[i].Articles) - pagesCh <- pageArchive // 重建归档 + PagesCh <- PageArchive // 重建归档 } return } @@ -261,15 +266,15 @@ func (c *Cache) rebuildArticle(article *model.Article, needSort bool) { Articles: model.SortedArticles{article}, }) if needSort { // 重建归档 - pagesCh <- pageArchive + PagesCh <- PageArchive } } // regeneratePages 重新生成series,archive页面 func (c *Cache) regeneratePages() { for { - switch page := <-pagesCh; page { - case pageSeries: + switch page := <-PagesCh; page { + case PageSeries: sort.Sort(c.Series) buf := bytes.Buffer{} buf.WriteString(c.Blogger.SeriesSay) @@ -288,7 +293,7 @@ func (c *Cache) regeneratePages() { buf.WriteString("\n\n") } c.PageSeries = string(render.RenderPage(buf.Bytes())) - case pageArchive: + case PageArchive: sort.Sort(c.Archives) buf := bytes.Buffer{} buf.WriteString(c.Blogger.ArchivesSay + "\n") diff --git a/pkg/cache/store/mongodb.go b/pkg/cache/store/mongodb.go index 7acd274..4c7a6e3 100644 --- a/pkg/cache/store/mongodb.go +++ b/pkg/cache/store/mongodb.go @@ -280,6 +280,21 @@ func (db *mongodb) RecoverArticle(ctx context.Context, id int) error { return err } +// LoadArticle 查找文章 +func (db *mongodb) LoadArticle(ctx context.Context, id int) (*model.Article, error) { + collection := db.Database(mongoDBName).Collection(collectionArticle) + + filter := bson.M{"id": id} + result := collection.FindOne(ctx, filter) + err := result.Err() + if err != nil { + return nil, err + } + article := &model.Article{} + err = result.Decode(article) + return article, err +} + // LoadAllArticle 读取所有文章 func (db *mongodb) LoadAllArticle(ctx context.Context) (model.SortedArticles, error) { collection := db.Database(mongoDBName).Collection(collectionArticle) diff --git a/pkg/cache/store/store.go b/pkg/cache/store/store.go index 2ac083c..c5f0b1e 100644 --- a/pkg/cache/store/store.go +++ b/pkg/cache/store/store.go @@ -48,6 +48,8 @@ type Store interface { UpdateArticle(ctx context.Context, id int, fields map[string]interface{}) error // RecoverArticle 恢复文章到草稿 RecoverArticle(ctx context.Context, id int) error + // LoadArticle 查找文章 + LoadArticle(ctx context.Context, id int) (*model.Article, error) // LoadAllArticle 读取所有文章 LoadAllArticle(ctx context.Context) (model.SortedArticles, error) // LoadTrashArticles 读取回收箱 diff --git a/pkg/core/blog/admin/admin.go b/pkg/core/blog/admin/admin.go index 0d63537..7f8fa2d 100644 --- a/pkg/core/blog/admin/admin.go +++ b/pkg/core/blog/admin/admin.go @@ -3,7 +3,9 @@ package admin import ( "context" + "fmt" "net/http" + "strconv" "time" "github.com/eiblog/eiblog/pkg/cache" @@ -14,6 +16,13 @@ import ( "github.com/sirupsen/logrus" ) +// 通知cookie +const ( + NoticeSuccess = "success" + NoticeNotice = "notice" + NoticeError = "error" +) + // RegisterRoutes register routes func RegisterRoutes(e *gin.Engine) { e.POST("/admin/login", handleAcctLogin) @@ -21,6 +30,9 @@ func RegisterRoutes(e *gin.Engine) { // RegisterRoutesAuthz register routes func RegisterRoutesAuthz(group gin.IRoutes) { + group.GET("/draft-delete", handleDraftDelete) + + group.POST("/api/account", handleAPIAccount) } // handleAcctLogin 登录接口 @@ -50,3 +62,150 @@ func handleAcctLogin(c *gin.Context) { }) c.Redirect(http.StatusFound, "/admin/profile") } + +// handleDraftDelete 删除草稿 +func handleDraftDelete(c *gin.Context) { + id, err := strconv.Atoi(c.Query("cid")) + if err != nil || id < 1 { + c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) + return + } + err = cache.Ei.RemoveArticle(context.Background(), id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "删除错误"}) + return + } + c.Redirect(http.StatusFound, "/admin/write-post") +} + +// handleAPIAccount 更新账户信息 +func handleAPIAccount(c *gin.Context) { + e := c.PostForm("email") + pn := c.PostForm("phoneNumber") + ad := c.PostForm("address") + if (e != "" && !tools.ValidateEmail(e)) || (pn != "" && + !tools.ValidatePhoneNo(pn)) { + responseNotice(c, NoticeNotice, "参数错误", "") + return + } + + err := cache.Ei.UpdateAccount(context.Background(), cache.Ei.Account.Username, + map[string]interface{}{ + "email": e, + "phone_n": pn, + "address": ad, + }) + if err != nil { + logrus.Error("handleAPIAccount.UpdateAccount: ", err) + responseNotice(c, NoticeNotice, err.Error(), "") + return + } + cache.Ei.Account.Email = e + cache.Ei.Account.PhoneN = pn + cache.Ei.Account.Address = ad + responseNotice(c, NoticeSuccess, "更新成功", "") +} + +// handleAPIBlogger 更新博客信息 +func handleAPIBlogger(c *gin.Context) { + bn := c.PostForm("blogName") + bt := c.PostForm("bTitle") + ba := c.PostForm("beiAn") + st := c.PostForm("subTitle") + ss := c.PostForm("seriessay") + as := c.PostForm("archivessay") + if bn == "" || bt == "" { + responseNotice(c, NoticeNotice, "参数错误", "") + return + } + + err := cache.Ei.UpdateBlogger(context.Background(), map[string]interface{}{ + "blog_name": bn, + "b_title": bt, + "sub_title": st, + "series_say": ss, + "archives_say": as, + }) + if err != nil { + logrus.Error("handleAPIBlogger.UpdateBlogger: ", err) + responseNotice(c, NoticeNotice, err.Error(), "") + return + } + cache.Ei.Blogger.BlogName = bn + cache.Ei.Blogger.BTitle = bt + cache.Ei.Blogger.BeiAn = ba + cache.Ei.Blogger.SubTitle = st + cache.Ei.Blogger.SeriesSay = ss + cache.Ei.Blogger.ArchivesSay = as + cache.PagesCh <- cache.PageSeries + cache.PagesCh <- cache.PageArchive + responseNotice(c, NoticeSuccess, "更新成功", "") +} + +// handleAPIPassword 更新密码 +func handleAPIPassword(c *gin.Context) { + od := c.PostForm("old") + nw := c.PostForm("new") + cf := c.PostForm("confirm") + if nw != cf { + responseNotice(c, NoticeNotice, "两次密码输入不一致", "") + return + } + if !tools.ValidatePassword(nw) { + responseNotice(c, NoticeNotice, "密码格式错误", "") + return + } + if cache.Ei.Account.Password != tools.EncryptPasswd(cache.Ei.Account.Username, od) { + responseNotice(c, NoticeNotice, "原始密码不正确", "") + return + } + newPwd := tools.EncryptPasswd(cache.Ei.Account.Username, nw) + + err := cache.Ei.UpdateAccount(context.Background(), cache.Ei.Account.Username, + map[string]interface{}{ + "password": newPwd, + }) + if err != nil { + logrus.Error("handleAPIPassword.UpdateAccount: ", err) + responseNotice(c, NoticeNotice, err.Error(), "") + return + } + cache.Ei.Account.Password = newPwd + responseNotice(c, NoticeSuccess, "更新成功", "") +} + +// handleAPIPostDelete 删除文章,移入回收箱 +func handleAPIPostDelete(c *gin.Context) { + // var ids []int32 + // for _, v := range c.PostFormArray("cid[]") { + // i, err := strconv.Atoi(v) + // if err != nil || int32(i) < config.Conf.BlogApp.General.StartID { + // responseNotice(c, NoticeNotice, "参数错误", "") + // return + // } + // ids = append(ids, int32(i)) + // } + // err := DelArticles(ids...) + // if err != nil { + // logd.Error(err) + // responseNotice(c, NOTICE_NOTICE, err.Error(), "") + // return + // } + // + // // elasticsearch + // err = ElasticDelIndex(ids) + // if err != nil { + // logrus.Error("handleAPIPostDelete.") + // } + // // TODO disqus delete + // responseNotice(c, NoticeSuccess, "删除成功", "") +} + +func responseNotice(c *gin.Context, typ, content, hl string) { + if hl != "" { + c.SetCookie("notice_highlight", hl, 86400, "/", "", true, false) + } + c.SetCookie("notice_type", typ, 86400, "/", "", true, false) + c.SetCookie("notice", fmt.Sprintf("[\"%s\"]", content), 86400, "/", "", true, false) + c.Redirect(http.StatusFound, c.Request.Referer()) +} diff --git a/pkg/core/blog/page/be.go b/pkg/core/blog/page/be.go index 7e7a3b6..9ab02af 100644 --- a/pkg/core/blog/page/be.go +++ b/pkg/core/blog/page/be.go @@ -3,12 +3,18 @@ package page import ( "bytes" + "context" + "encoding/json" + "fmt" htemplate "html/template" "net/http" + "strconv" "github.com/eiblog/eiblog/pkg/cache" "github.com/eiblog/eiblog/pkg/config" "github.com/eiblog/eiblog/pkg/core/blog" + "github.com/eiblog/eiblog/pkg/model" + "github.com/sirupsen/logrus" "github.com/gin-gonic/gin" ) @@ -34,6 +40,181 @@ func handleLoginPage(c *gin.Context) { renderHTMLAdminLayout(c, "login.html", params) } +// handleAdminProfile 个人配置 +func handleAdminProfile(c *gin.Context) { + params := baseBEParams(c) + params["Title"] = "个人配置 | " + cache.Ei.Blogger.BTitle + params["Path"] = c.Request.URL.Path + params["Console"] = true + params["Ei"] = cache.Ei + renderHTMLAdminLayout(c, "admin-profile", params) +} + +type T struct { + ID string `json:"id"` + Tags string `json:"tags"` +} + +// handleAdminPost 写文章页 +func handleAdminPost(c *gin.Context) { + params := baseBEParams(c) + id, err := strconv.Atoi(c.Query("cid")) + if err == nil && id > 0 { + article, _ := cache.Ei.LoadArticle(context.Background(), id) + if article != nil { + params["Title"] = "编辑文章 | " + cache.Ei.Blogger.BTitle + params["Edit"] = article + } + } + if params["Title"] == nil { + params["Title"] = "撰写文章 | " + cache.Ei.Blogger.BTitle + } + params["Path"] = c.Request.URL.Path + params["Domain"] = config.Conf.BlogApp.Host + params["Series"] = cache.Ei.Series + var tags []T + for tag, _ := range cache.Ei.TagArticles { + tags = append(tags, T{tag, tag}) + } + str, _ := json.Marshal(tags) + params["Tags"] = string(str) + renderHTMLAdminLayout(c, "admin-post", params) +} + +// handleAdminPosts 文章管理页 +func handleAdminPosts(c *gin.Context) { + kw := c.Query("keywords") + tmp := c.Query("serie") + se, err := strconv.Atoi(tmp) + if err != nil || se < 1 { + se = 0 + } + pg, err := strconv.Atoi(c.Query("page")) + if err != nil || pg < 1 { + pg = 1 + } + vals := c.Request.URL.Query() + + params := baseBEParams(c) + params["Title"] = "文章管理 | " + cache.Ei.Blogger.BTitle + params["Manage"] = true + params["Path"] = c.Request.URL.Path + params["Series"] = cache.Ei.Series + params["Serie"] = se + params["KW"] = kw + var max int + // TODO + // max, params["List"] = cache.Ei.PageListBack(se, kw, false, false, pg, setting.Conf.General.PageSize) + if pg < max { + vals.Set("page", fmt.Sprint(pg+1)) + params["Next"] = vals.Encode() + } + if pg > 1 { + vals.Set("page", fmt.Sprint(pg-1)) + params["Prev"] = vals.Encode() + } + params["PP"] = make(map[int]string, max) + for i := 0; i < max; i++ { + vals.Set("page", fmt.Sprint(i+1)) + params["PP"].(map[int]string)[i+1] = vals.Encode() + } + params["Cur"] = pg + renderHTMLAdminLayout(c, "admin-posts", params) +} + +// handleAdminSeries 专题列表 +func handleAdminSeries(c *gin.Context) { + params := baseBEParams(c) + params["Title"] = "专题管理 | " + cache.Ei.Blogger.BTitle + params["Manage"] = true + params["Path"] = c.Request.URL.Path + params["List"] = cache.Ei.Series + renderHTMLAdminLayout(c, "admin-series", params) +} + +// handleAdminSerie 编辑专题 +func handleAdminSerie(c *gin.Context) { + params := baseBEParams(c) + + id, err := strconv.Atoi(c.Query("mid")) + params["Title"] = "新增专题 | " + cache.Ei.Blogger.BTitle + if err == nil && id > 0 { + var serie *model.Serie + for _, v := range cache.Ei.Series { + if v.ID == id { + params["Title"] = "编辑专题 | " + cache.Ei.Blogger.BTitle + params["Edit"] = serie + break + } + } + } + params["Manage"] = true + params["Path"] = c.Request.URL.Path + renderHTMLAdminLayout(c, "admin-serie", params) +} + +// handleAdminTags 标签列表 +func handleAdminTags(c *gin.Context) { + params := baseBEParams(c) + params["Title"] = "标签管理 | " + cache.Ei.Blogger.BTitle + params["Manage"] = true + params["Path"] = c.Request.URL.Path + params["List"] = cache.Ei.TagArticles + renderHTMLAdminLayout(c, "admin-tags", params) +} + +// handleAdminDraft 草稿箱页 +func handleAdminDraft(c *gin.Context) { + params := baseBEParams(c) + + params["Title"] = "草稿箱 | " + cache.Ei.Blogger.BTitle + params["Manage"] = true + params["Path"] = c.Request.URL.Path + var err error + params["List"], err = cache.Ei.LoadDraftArticles(context.Background()) + if err != nil { + logrus.Error("handleDraft.LoadDraftArticles: ", err) + c.Status(http.StatusBadRequest) + } else { + c.Status(http.StatusOK) + } + renderHTMLAdminLayout(c, "admin-draft", params) +} + +// handleAdminTrash 回收箱页 +func handleAdminTrash(c *gin.Context) { + params := baseBEParams(c) + params["Title"] = "回收箱 | " + cache.Ei.Blogger.BTitle + params["Manage"] = true + params["Path"] = c.Request.URL.Path + var err error + params["List"], err = cache.Ei.LoadTrashArticles(context.Background()) + if err != nil { + logrus.Error("handleTrash.LoadTrashArticles: ", err) + c.HTML(http.StatusBadRequest, "backLayout.html", params) + return + } + renderHTMLAdminLayout(c, "admin-trash", params) +} + +// handleAdminGeneral 基本设置 +func handleAdminGeneral(c *gin.Context) { + params := baseBEParams(c) + params["Title"] = "基本设置 | " + cache.Ei.Blogger.BTitle + params["Setting"] = true + params["Path"] = c.Request.URL.Path + renderHTMLAdminLayout(c, "admin-general", params) +} + +// handleAdminDiscussion 阅读设置 +func handleAdminDiscussion(c *gin.Context) { + params := baseBEParams(c) + params["Title"] = "阅读设置 | " + cache.Ei.Blogger.BTitle + params["Setting"] = true + params["Path"] = c.Request.URL.Path + renderHTMLAdminLayout(c, "admin-discussion", params) +} + // renderHTMLAdminLayout 渲染admin页面 func renderHTMLAdminLayout(c *gin.Context, name string, data gin.H) { c.Header("Content-Type", "text/html; charset=utf-8") diff --git a/pkg/core/blog/page/page.go b/pkg/core/blog/page/page.go index 25ed60c..3406f20 100644 --- a/pkg/core/blog/page/page.go +++ b/pkg/core/blog/page/page.go @@ -50,5 +50,17 @@ func RegisterRoutes(e *gin.Engine) { // RegisterRoutesAuthz register admin func RegisterRoutesAuthz(group gin.IRoutes) { - + // console + group.GET("/profile", handleAdminProfile) + // write + group.GET("/write-post", handleAdminPost) + // manage + group.GET("/manage-posts", handleAdminPosts) + group.GET("/manage-series", handleAdminSeries) + group.GET("/add-serie", handleAdminSerie) + group.GET("/manage-tags", handleAdminTags) + group.GET("/manage-draft", handleAdminDraft) + group.GET("/manage-trash", handleAdminTrash) + group.GET("/options-general", handleAdminGeneral) + group.GET("/options-discussion", handleAdminDiscussion) } diff --git a/tools/validate.go b/tools/validate.go new file mode 100644 index 0000000..c96c5c1 --- /dev/null +++ b/tools/validate.go @@ -0,0 +1,23 @@ +// Package tools provides ... +package tools + +import "regexp" + +var regexpEmail = regexp.MustCompile(`^(\w)+([\.\-]\w+)*@(\w)+((\.\w+)+)$`) + +// ValidateEmail 校验邮箱 +func ValidateEmail(e string) bool { + return regexpEmail.MatchString(e) +} + +var regexpPhoneNo = regexp.MustCompile(`^\+\d+$`) + +// ValidatePhoneNo 校验手机号 +func ValidatePhoneNo(no string) bool { + return regexpPhoneNo.MatchString(no) +} + +// ValidatePassword 校验米阿莫 +func ValidatePassword(pwd string) bool { + return len(pwd) > 5 && len(pwd) < 32 +} diff --git a/website/admin/profile.html b/website/admin/profile.html index 50f6602..0487e95 100644 --- a/website/admin/profile.html +++ b/website/admin/profile.html @@ -3,15 +3,15 @@
-
+
{{.SubTitle}}
-最后登录: {{dateformat .LoginAt "2006/01/02 15:04"}}
+{{.Blogger.SubTitle}}
+最后登录: {{dateformat .Account.LoginAt "2006/01/02 15:04"}}
用于发送告警邮件及其它通知, 建议填写, 如: example@163.com.
选择填写, 如: +8615123456789.
选择填写, 如: xx省xx市xx区(县)xxxx小区xxx号.
用户昵称可以与用户名不同, 用于前台显示.
如果你将此项留空, 将默认使用登录用户名.
用于所有页面的title组成, 如: Deepzz's Blog
简介或格言, 如: 生活百般滋味, 人生需要笑对.
用于底部显示, 不添加则不显示, 如: 蜀 ICP 备 16021362 号
此文字用于专题前述, 会在专题最前方显示.
此文字用于归档前述, 会在归档最前方显示.