实现找回密码功能

pull/25/merge
Minho 2017-05-03 14:22:05 +08:00
parent ff7dfadcfe
commit 5c535f5bff
38 changed files with 970 additions and 73 deletions

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ _testmain.go
*.test *.test
*.prof *.prof
.idea .idea
/conf/app.conf

View File

@ -26,7 +26,7 @@ func RegisterDataBase() {
port := beego.AppConfig.String("db_port") port := beego.AppConfig.String("db_port")
dataSource := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=%s",username,password,host,port,database,url.QueryEscape(timezone)) dataSource := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=%s",username,password,host,port,database,url.QueryEscape(timezone))
orm.RegisterDataBase("default", "mysql", dataSource) orm.RegisterDataBase("default", "mysql", dataSource)
@ -47,22 +47,23 @@ func RegisterModel() {
new(models.Attachment), new(models.Attachment),
new(models.Logger), new(models.Logger),
new(models.CommentVote), new(models.CommentVote),
new(models.MemberToken),
) )
} }
func Initialization() { func Initialization() {
o := orm.NewOrm() //o := orm.NewOrm()
o.Raw("alter table "+models.NewMember().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec() //o.Raw("alter table "+models.NewMember().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
o.Raw("alter table "+models.NewBook().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec() //o.Raw("alter table "+models.NewBook().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
o.Raw("alter table "+models.NewRelationship().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec() //o.Raw("alter table "+models.NewRelationship().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
o.Raw("alter table "+models.NewComment().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec() //o.Raw("alter table "+models.NewComment().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
o.Raw("alter table " +models.NewOption().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec() //o.Raw("alter table " +models.NewOption().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
o.Raw("alter table "+models.NewDocument().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec() //o.Raw("alter table "+models.NewDocument().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
o.Raw("alter table "+models.NewAttachment().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec() //o.Raw("alter table "+models.NewAttachment().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
o.Raw("alter table "+models.NewLogger().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec() //o.Raw("alter table "+models.NewLogger().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
o.Raw("alter table "+models.NewCommentVote().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec() //o.Raw("alter table "+models.NewCommentVote().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
options := []models.Option { options := []models.Option {
{ OptionName: "ENABLED_CAPTCHA", OptionValue: "false", OptionTitle:"是否启用验证码"}, { OptionName: "ENABLED_CAPTCHA", OptionValue: "false", OptionTitle:"是否启用验证码"},
@ -91,7 +92,6 @@ func RegisterLogger() {
logs.EnableFuncCallDepth(true) logs.EnableFuncCallDepth(true)
logs.Async() logs.Async()
//beego.BeeLogger.DelLogger("console")
if _,err := os.Stat("logs/log.log"); os.IsNotExist(err) { if _,err := os.Stat("logs/log.log"); os.IsNotExist(err) {
if f,err := os.Create("logs/log.log");err == nil { if f,err := os.Create("logs/log.log");err == nil {
f.Close() f.Close()

View File

@ -1,31 +0,0 @@
appname = godoc
httpport = 8181
runmode = dev
sessionon = true
sessionname = mindoc_id
copyrequestbody = true
#默认Session生成Key的秘钥
beegoserversessionkey=123456
#Session储存方式
sessionprovider=file
sessionproviderconfig=./logs
#时区设置
timezone = Asia/Shanghai
#数据库配置
db_host=127.0.0.1
db_port=3306
db_database=mindoc_db
db_username=root
db_password=123456
#项目默认封面
cover=/static/images/book.jpg
#默认编辑器
editor=markdown
#上传文件的后缀
upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif

View File

@ -35,3 +35,21 @@ token_size=12
#上传文件的后缀 #上传文件的后缀
upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif upload_file_ext=txt|doc|docx|xls|xlsx|ppt|pptx|pdf|7z|rar|jpg|jpeg|png|gif
####################邮件配置######################
#是否启用邮件
enable_mail=false
#每小时限制指定邮箱邮件发送次数
mail_number=5
#smtp服务用户名
smtp_user_name=admin@iminho.me
#smtp服务器地址
smtp_host=smtp.ym.163.com
#smtp密码
smtp_password=
#端口号
smtp_port=25
#发送邮件的显示名称
form_user_name=admin@iminho.me
#邮件有效期30分钟
mail_expired=30

View File

@ -52,14 +52,17 @@ func GetDefaultAvatar() string {
return beego.AppConfig.DefaultString("avatar","/static/images/headimgurl.jpg") return beego.AppConfig.DefaultString("avatar","/static/images/headimgurl.jpg")
} }
//获取阅读令牌长度.
func GetTokenSize() int { func GetTokenSize() int {
return beego.AppConfig.DefaultInt("token_size",12) return beego.AppConfig.DefaultInt("token_size",12)
} }
//获取默认文档封面.
func GetDefaultCover() string { func GetDefaultCover() string {
return beego.AppConfig.DefaultString("cover","/static/images/book.jpg") return beego.AppConfig.DefaultString("cover","/static/images/book.jpg")
} }
//获取允许的商城文件的类型.
func GetUploadFileExt() []string { func GetUploadFileExt() []string {
ext := beego.AppConfig.DefaultString("upload_file_ext","png|jpg|jpeg|gif|txt|doc|docx|pdf") ext := beego.AppConfig.DefaultString("upload_file_ext","png|jpg|jpeg|gif|txt|doc|docx|pdf")
@ -76,7 +79,7 @@ func GetUploadFileExt() []string {
} }
return exts return exts
} }
//判断是否是允许商城的文件类型.
func IsAllowUploadFileExt(ext string) bool { func IsAllowUploadFileExt(ext string) bool {
if strings.HasPrefix(ext,".") { if strings.HasPrefix(ext,".") {
@ -91,3 +94,8 @@ func IsAllowUploadFileExt(ext string) bool {
} }
return false return false
} }
//获取当前版本.
func Version() string {
return "v0.1"
}

38
conf/mail.go 100644
View File

@ -0,0 +1,38 @@
package conf
import (
"github.com/astaxie/beego"
"strings"
)
type SmtpConf struct {
EnableMail bool
MailNumber int
SmtpUserName string
SmtpHost string
SmtpPassword string
SmtpPort int
FormUserName string
MailExpired int
}
func GetMailConfig() *SmtpConf {
user_name := beego.AppConfig.String("smtp_user_name")
password := beego.AppConfig.String("smtp_password")
smtp_host := beego.AppConfig.String("smtp_host")
smtp_port := beego.AppConfig.DefaultInt("smtp_port",25)
form_user_name := beego.AppConfig.String("form_user_name")
enable_mail := beego.AppConfig.String("enable_mail")
mail_number := beego.AppConfig.DefaultInt("mail_number",5)
c := &SmtpConf{
EnableMail : strings.EqualFold(enable_mail,"true"),
MailNumber: mail_number,
SmtpUserName:user_name,
SmtpHost:smtp_host,
SmtpPassword:password,
FormUserName:form_user_name,
SmtpPort:smtp_port,
}
return c
}

View File

@ -3,15 +3,16 @@ package controllers
import ( import (
"time" "time"
"strings" "strings"
"regexp"
"net/smtp"
"github.com/lifei6671/godoc/conf" "github.com/lifei6671/godoc/conf"
"github.com/lifei6671/godoc/models" "github.com/lifei6671/godoc/models"
"github.com/lifei6671/godoc/utils" "github.com/lifei6671/godoc/utils"
"github.com/astaxie/beego" "github.com/astaxie/beego"
"github.com/astaxie/beego/logs" "github.com/astaxie/beego/logs"
"github.com/lifei6671/gocaptcha" "github.com/lifei6671/gocaptcha"
"strconv"
"regexp"
) )
// AccountController 用户登录与注册. // AccountController 用户登录与注册.
@ -30,10 +31,7 @@ func (c *AccountController) Login() {
if cookie,ok := c.GetSecureCookie(conf.GetAppKey(),"login");ok{ if cookie,ok := c.GetSecureCookie(conf.GetAppKey(),"login");ok{
if err := utils.Decode(cookie,&remember); err == nil { if err := utils.Decode(cookie,&remember); err == nil {
member := models.NewMember() if member,err := models.NewMember().Find(remember.MemberId); err == nil {
member.MemberId = remember.MemberId
if err := models.NewMember().Find(remember.MemberId); err == nil {
c.SetMember(*member) c.SetMember(*member)
c.Redirect(beego.URLFor("HomeController.Index"), 302) c.Redirect(beego.URLFor("HomeController.Index"), 302)
@ -137,9 +135,193 @@ func (c *AccountController) Register() {
} }
} }
func (p *AccountController) FindPassword() { //找回密码.
p.TplName = "account/find_password.tpl" func (c *AccountController) FindPassword() {
c.TplName = "account/find_password_setp1.tpl"
mail_conf := conf.GetMailConfig()
if c.Ctx.Input.IsPost() {
email := c.GetString("email")
captcha := c.GetString("code")
if email == "" {
c.JsonResult(6005,"邮箱地址不能为空")
}
if !mail_conf.EnableMail {
c.JsonResult(6004,"未启用邮件服务")
}
//如果开启了验证码
if v,ok := c.Option["ENABLED_CAPTCHA"]; ok && strings.EqualFold(v,"true") {
v,ok := c.GetSession(conf.CaptchaSessionName).(string);
if !ok || !strings.EqualFold(v,captcha){
c.JsonResult(6001,"验证码不正确")
}
}
member ,err := models.NewMember().FindByFieldFirst("email",email)
if err != nil {
c.JsonResult(6006,"邮箱不存在")
}
if member.Status != 0 {
c.JsonResult(6007,"账号已被禁用")
}
count,err := models.NewMemberToken().FindSendCount(email,time.Now().Add(-1*time.Hour),time.Now())
if err != nil {
beego.Error(err)
c.JsonResult(6008,"发送邮件失败")
}
if count > mail_conf.MailNumber {
c.JsonResult(6008,"发送次数太多,请稍候再试")
}
member_token := models.NewMemberToken()
member_token.Token = string(utils.Krand(32,utils.KC_RAND_KIND_ALL))
member_token.Email = email
member_token.MemberId = member.MemberId
member_token.IsValid = false
if _,err := member_token.InsertOrUpdate(); err != nil {
c.JsonResult(6009,"邮件发送失败")
}
data := map[string]interface{}{
"SITE_NAME" : c.Option["SITE_NAME"],
"url" : c.BaseUrl() + beego.URLFor("AccountController.FindPassword", "token",member_token.Token,"mail",email),
}
body,err := c.ExecuteViewPathTemplate("account/mail_template.tpl",data)
if err != nil {
beego.Error(err)
c.JsonResult(6003,"邮件发送失败")
}
go func(mail_conf *conf.SmtpConf,email string,body string) {
auth := smtp.PlainAuth(
"",
mail_conf.SmtpUserName,
mail_conf.SmtpPassword,
mail_conf.SmtpHost,
)
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n";
subject := "Subject: 找回密码!\n"
err = smtp.SendMail(
mail_conf.SmtpHost + ":" + strconv.Itoa(mail_conf.SmtpPort),
auth,
mail_conf.FormUserName,
[]string{ email },
[]byte(subject + mime +"\n" +body),
)
if err != nil {
beego.Error("邮件发送失败 => ",email,err)
}
}(mail_conf,email,body)
c.JsonResult(0,"ok", c.BaseUrl() + beego.URLFor("AccountController.Login"))
}
token := c.GetString("token")
mail := c.GetString("mail")
if token != "" && mail != "" {
member_token,err := models.NewMemberToken().FindByFieldFirst("token",token)
if err != nil {
beego.Error(err)
c.Data["ErrorMessage"] = "邮件已失效"
c.TplName = "errors/error.tpl"
return
}
sub_time := member_token.SendTime.Sub(time.Now())
if !strings.EqualFold(member_token.Email,mail) || sub_time.Minutes() > float64(mail_conf.MailExpired) || !member_token.ValidTime.IsZero() {
c.Data["ErrorMessage"] = "验证码已过期,请重新操作。"
c.TplName = "errors/error.tpl"
return
}
c.Data["Email"] = member_token.Email
c.Data["Token"] = member_token.Token
c.TplName = "account/find_password_setp2.tpl"
}
} }
//校验邮件并修改密码.
func (c *AccountController) ValidEmail() {
c.Prepare()
password1 := c.GetString("password1")
password2 := c.GetString("password2")
captcha := c.GetString("code")
token := c.GetString("token")
mail := c.GetString("mail")
if password1 == "" {
c.JsonResult(6001,"密码不能为空")
}
if l := strings.Count(password1,""); l <6 || l > 50{
c.JsonResult(6001,"密码不能为空且必须在6-50个字符之间")
}
if password2 == ""{
c.JsonResult(6002,"确认密码不能为空")
}
if password1 != password2 {
c.JsonResult(6003,"确认密码输入不正确")
}
if captcha == "" {
c.JsonResult(6004,"验证码不能为空")
}
v,ok := c.GetSession(conf.CaptchaSessionName).(string);
if !ok || !strings.EqualFold(v,captcha){
c.JsonResult(6001,"验证码不正确")
}
mail_conf := conf.GetMailConfig()
member_token,err := models.NewMemberToken().FindByFieldFirst("token",token)
if err != nil {
beego.Error(err)
c.JsonResult(6007,"邮件已失效")
}
sub_time := member_token.SendTime.Sub(time.Now())
if !strings.EqualFold(member_token.Email,mail) || sub_time.Minutes() > float64(mail_conf.MailExpired) || !member_token.ValidTime.IsZero() {
c.JsonResult(6008,"验证码已过期,请重新操作。")
}
member ,err := models.NewMember().Find(member_token.MemberId)
if err != nil{
beego.Error(err)
c.JsonResult(6005,"用户不存在")
}
hash ,err := utils.PasswordHash(password1);
if err != nil {
beego.Error(err)
c.JsonResult(6006,"保存密码失败")
}
member.Password = hash
err = member.Update("password")
member_token.ValidTime = time.Now()
member_token.IsValid = true
member_token.InsertOrUpdate()
if err != nil {
beego.Error(err)
c.JsonResult(6006,"保存密码失败")
}
c.JsonResult(0,"ok",c.BaseUrl() + beego.URLFor("AccountController.Login"))
}
// Logout 退出登录. // Logout 退出登录.
func (c *AccountController) Logout(){ func (c *AccountController) Logout(){
c.SetMember(models.Member{}); c.SetMember(models.Member{});

View File

@ -8,6 +8,8 @@ import (
"github.com/lifei6671/godoc/conf" "github.com/lifei6671/godoc/conf"
"github.com/astaxie/beego" "github.com/astaxie/beego"
"strings" "strings"
"encoding/json"
"io"
) )
@ -62,17 +64,25 @@ func (c *BaseController) SetMember(member models.Member) {
// JsonResult 响应 json 结果 // JsonResult 响应 json 结果
func (c *BaseController) JsonResult(errCode int,errMsg string,data ...interface{}){ func (c *BaseController) JsonResult(errCode int,errMsg string,data ...interface{}){
json := make(map[string]interface{},3) jsonData := make(map[string]interface{},3)
json["errcode"] = errCode jsonData["errcode"] = errCode
json["message"] = errMsg jsonData["message"] = errMsg
if len(data) > 0 && data[0] != nil{ if len(data) > 0 && data[0] != nil{
json["data"] = data[0] jsonData["data"] = data[0]
} }
c.Data["json"] = json returnJSON, err := json.Marshal(jsonData)
c.ServeJSON(true)
if err != nil {
beego.Error(err)
}
c.Ctx.ResponseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
io.WriteString(c.Ctx.ResponseWriter,string(returnJSON))
c.StopRun() c.StopRun()
} }

View File

@ -88,7 +88,7 @@ func (c *BookMemberController) ChangeRole() {
member := models.NewMember() member := models.NewMember()
if err := member.Find(member_id); err != nil { if _,err := member.Find(member_id); err != nil {
c.JsonResult(6003,"用户不存在") c.JsonResult(6003,"用户不存在")
} }
if member.Status == 1 { if member.Status == 1 {

View File

@ -0,0 +1,17 @@
package controllers
type ErrorController struct {
BaseController
}
func (c *ErrorController) Error404() {
c.TplName = "errors/404.tpl"
}
func (c *ErrorController) Error403() {
c.TplName = "errors/403.tpl"
}
func (c *ErrorController) Error500() {
c.TplName = "errors/error.tpl"
}

View File

@ -139,7 +139,7 @@ func (c *ManagerController) UpdateMemberStatus() {
} }
member := models.NewMember() member := models.NewMember()
if err := member.Find(member_id); err != nil { if _,err := member.Find(member_id); err != nil {
c.JsonResult(6002,"用户不存在") c.JsonResult(6002,"用户不存在")
} }
member.Status = status member.Status = status
@ -168,7 +168,7 @@ func (c *ManagerController) ChangeMemberRole() {
} }
member := models.NewMember() member := models.NewMember()
if err := member.Find(member_id); err != nil { if _,err := member.Find(member_id); err != nil {
c.JsonResult(6002,"用户不存在") c.JsonResult(6002,"用户不存在")
} }
member.Role = role member.Role = role

View File

@ -141,8 +141,7 @@ func (c *SettingController) Upload() {
url := "/" + filePath url := "/" + filePath
member := models.NewMember() if member,err := models.NewMember().Find(c.Member.MemberId);err == nil {
if err := member.Find(c.Member.MemberId);err == nil {
member.Avatar = url member.Avatar = url
member.Update() member.Update()
c.SetMember(*member) c.SetMember(*member)

View File

@ -7,6 +7,7 @@ import (
"github.com/lifei6671/godoc/commands" "github.com/lifei6671/godoc/commands"
"fmt" "fmt"
"os" "os"
"github.com/lifei6671/godoc/controllers"
) )
func main() { func main() {
@ -21,5 +22,7 @@ func main() {
fmt.Println(os.Args[0]) fmt.Println(os.Args[0])
beego.ErrorController(&controllers.ErrorController{})
beego.Run() beego.Run()
} }

View File

@ -73,9 +73,7 @@ func (m *BookResult) FindByIdentify(identify string,member_id int) (*BookResult,
return m, ErrPermissionDenied return m, ErrPermissionDenied
} }
member := NewMember() member, err := NewMember().Find(relationship2.MemberId)
err = member.Find(relationship2.MemberId)
if err != nil { if err != nil {
return m, err return m, err
} }

View File

@ -118,7 +118,7 @@ func (m *Comment) Insert() error {
if m.MemberId > 0 { if m.MemberId > 0 {
member := NewMember() member := NewMember()
//如果用户不存在 //如果用户不存在
if err := member.Find(m.MemberId) ; err != nil { if _,err := member.Find(m.MemberId) ; err != nil {
return ErrMemberNoExist return ErrMemberNoExist
} }
//如果用户被禁用 //如果用户被禁用

View File

@ -7,6 +7,9 @@ import (
"github.com/lifei6671/godoc/utils" "github.com/lifei6671/godoc/utils"
"github.com/lifei6671/godoc/conf" "github.com/lifei6671/godoc/conf"
"github.com/astaxie/beego/logs" "github.com/astaxie/beego/logs"
"errors"
"regexp"
"strings"
) )
type Member struct { type Member struct {
@ -14,7 +17,7 @@ type Member struct {
Account string `orm:"size(100);unique;column(account)" json:"account"` Account string `orm:"size(100);unique;column(account)" json:"account"`
Password string `orm:"size(1000);column(password)" json:"-"` Password string `orm:"size(1000);column(password)" json:"-"`
Description string `orm:"column(description);size(2000)" json:"description"` Description string `orm:"column(description);size(2000)" json:"description"`
Email string `orm:"size(255);column(email);null;default(null)" json:"email"` Email string `orm:"size(255);column(email);unique" json:"email"`
Phone string `orm:"size(255);column(phone);null;default(null)" json:"phone"` Phone string `orm:"size(255);column(phone);null;default(null)" json:"phone"`
Avatar string `orm:"size(1000);column(avatar)" json:"avatar"` Avatar string `orm:"size(1000);column(avatar)" json:"avatar"`
//用户角色0 超级管理员 /1 管理员/ 2 普通用户 . //用户角色0 超级管理员 /1 管理员/ 2 普通用户 .
@ -71,6 +74,22 @@ func (m *Member) Login(account string,password string) (*Member,error) {
func (m *Member) Add () (error) { func (m *Member) Add () (error) {
o := orm.NewOrm() o := orm.NewOrm()
if ok,err := regexp.MatchString(conf.RegexpAccount,m.Account); m.Account == "" || !ok || err != nil {
return errors.New("账号只能由英文字母数字组成且在3-50个字符")
}
if m.Email == "" {
return errors.New("邮箱不能为空")
}
if ok,err := regexp.MatchString(conf.RegexpEmail,m.Email); !ok || err != nil || m.Email == "" {
return errors.New("邮箱格式不正确")
}
if l := strings.Count(m.Password,""); l <6 || l > 50{
return errors.New("密码不能为空且必须在6-50个字符之间")
}
if c,err := o.QueryTable(m.TableNameWithPrefix()).Filter("email",m.Email).Count(); err == nil || c > 0 {
return errors.New("邮箱已被使用")
}
hash ,err := utils.PasswordHash(m.Password); hash ,err := utils.PasswordHash(m.Password);
if err != nil { if err != nil {
@ -92,21 +111,24 @@ func (m *Member) Add () (error) {
func (m *Member) Update(cols... string) (error) { func (m *Member) Update(cols... string) (error) {
o := orm.NewOrm() o := orm.NewOrm()
if m.Email == "" {
return errors.New("邮箱不能为空")
}
if _,err := o.Update(m,cols...);err != nil { if _,err := o.Update(m,cols...);err != nil {
return err return err
} }
return nil return nil
} }
func (m *Member) Find(id int) error{ func (m *Member) Find(id int) (*Member,error){
o := orm.NewOrm() o := orm.NewOrm()
m.MemberId = id m.MemberId = id
if err := o.Read(m); err != nil { if err := o.Read(m); err != nil {
return err return m,err
} }
m.ResolveRoleName() m.ResolveRoleName()
return nil return m,nil
} }
func (m *Member) ResolveRoleName (){ func (m *Member) ResolveRoleName (){
@ -163,7 +185,13 @@ func (c *Member) IsAdministrator() bool {
return c.Role == 0 || c.Role == 1 return c.Role == 0 || c.Role == 1
} }
func (m *Member) FindByFieldFirst(field string,value interface{}) (*Member,error) {
o := orm.NewOrm()
err := o.QueryTable(m.TableNameWithPrefix()).Filter(field,value).OrderBy("-member_id").One(m)
return m,err
}

View File

@ -0,0 +1,66 @@
package models
import (
"time"
"github.com/lifei6671/godoc/conf"
"github.com/astaxie/beego/orm"
)
type MemberToken struct {
TokenId int `orm:"column(token_id);pk;auto;unique" json:"token_id"`
MemberId int `orm:"column(member_id);type(int)" json:"member_id"`
Token string `orm:"column(token);size(255);index" json:"token"`
Email string `orm:"column(email);size(255)" json:"email"`
IsValid bool `orm:"column(is_valid)" json:"is_valid"`
ValidTime time.Time `orm:"column(valid_time);null" json:"valid_time"`
SendTime time.Time `orm:"column(send_time);auto_now_add;type(datetime)" json:"send_time"`
}
// TableName 获取对应数据库表名.
func (m *MemberToken) TableName() string {
return "member_token"
}
// TableEngine 获取数据使用的引擎.
func (m *MemberToken) TableEngine() string {
return "INNODB"
}
func (m *MemberToken)TableNameWithPrefix() string {
return conf.GetDatabasePrefix() + m.TableName()
}
func NewMemberToken() *MemberToken {
return &MemberToken{}
}
func (m *MemberToken) InsertOrUpdate() (*MemberToken,error){
o := orm.NewOrm()
if m.TokenId > 0 {
_,err := o.Update(m)
return m,err
}
_,err := o.Insert(m)
return m,err
}
func (m *MemberToken) FindByFieldFirst(field string,value interface{}) (*MemberToken,error) {
o := orm.NewOrm()
err := o.QueryTable(m.TableNameWithPrefix()).Filter(field,value).OrderBy("-token_id").One(m)
return m,err
}
func (m *MemberToken) FindSendCount(mail string,start_time time.Time,end_time time.Time) (int ,error) {
o := orm.NewOrm()
c,err := o.QueryTable(m.TableNameWithPrefix()).Filter("send_time__gte",start_time.Format("2006-01-02 15:04:05")).Filter("send_time__lte",end_time.Format("2006-01-02 15:04:05")).Count()
if err != nil {
return 0,err
}
return int(c),nil
}

View File

@ -22,4 +22,11 @@ func init() {
beego.InsertFilter("/book",beego.BeforeRouter,FilterUser) beego.InsertFilter("/book",beego.BeforeRouter,FilterUser)
beego.InsertFilter("/book/*",beego.BeforeRouter,FilterUser) beego.InsertFilter("/book/*",beego.BeforeRouter,FilterUser)
beego.InsertFilter("/api/*",beego.BeforeRouter,FilterUser) beego.InsertFilter("/api/*",beego.BeforeRouter,FilterUser)
var FinishRouter = func(ctx *context.Context) {
ctx.ResponseWriter.Header().Add("MinDoc-Version",conf.Version())
ctx.ResponseWriter.Header().Add("MinDoc-Site","http://www.iminho.me")
}
beego.InsertFilter("/*",beego.BeforeRouter ,FinishRouter, false)
} }

View File

@ -12,6 +12,7 @@ func init() {
beego.Router("/logout", &controllers.AccountController{},"*:Logout") beego.Router("/logout", &controllers.AccountController{},"*:Logout")
beego.Router("/register", &controllers.AccountController{},"*:Register") beego.Router("/register", &controllers.AccountController{},"*:Register")
beego.Router("/find_password", &controllers.AccountController{},"*:FindPassword") beego.Router("/find_password", &controllers.AccountController{},"*:FindPassword")
beego.Router("/valid_email", &controllers.AccountController{},"post:ValidEmail")
beego.Router("/captcha", &controllers.AccountController{},"*:Captcha") beego.Router("/captcha", &controllers.AccountController{},"*:Captcha")
beego.Router("/manager", &controllers.ManagerController{},"*:Index") beego.Router("/manager", &controllers.ManagerController{},"*:Index")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,16 @@
/* latin-ext */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 100;
src: local('Lato Hairline'), local('Lato-Hairline'), url(lato/v11/eFRpvGLEW31oiexbYNx7Y_esZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 100;
src: local('Lato Hairline'), local('Lato-Hairline'), url(lato/v11/GtRkRNTnri0g82CjKnEB0Q.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}

View File

@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="/favicon.ico">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="SmartWiki" />
<title>找回密码 - Powered by MinDoc</title>
<!-- Bootstrap -->
<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link href="/static/css/main.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="/static/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="/static/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="/static/jquery/1.12.4/jquery.min.js"></script>
</head>
<body class="manual-container">
<header class="navbar navbar-static-top smart-nav navbar-fixed-top manual-header" role="banner">
<div class="container">
<div class="navbar-header col-sm-12 col-md-6 col-lg-5">
<a href="/" class="navbar-brand">MinDoc</a>
</div>
</div>
</header>
<div class="container manual-body">
<div class="row login">
<div class="login-body">
<form role="form" method="post" id="findPasswordForm">
<h3 class="text-center">找回密码</h3>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-at"></i>
</div>
<input type="text" class="form-control" placeholder="邮箱" name="email" id="email" autocomplete="off">
</div>
</div>
<div class="form-group">
<div class="input-group" style="float: left;width: 195px;">
<div class="input-group-addon">
<i class="fa fa-check-square"></i>
</div>
<input type="text" name="code" id="code" class="form-control" style="width: 150px" maxlength="5" placeholder="验证码" autocomplete="off">&nbsp;
</div>
<img id="captcha-img" style="width: 140px;height: 40px;display: inline-block;float: right" src="{{urlfor "AccountController.Captcha"}}" onclick="this.src='{{urlfor "AccountController.Captcha"}}?key=login&t='+(new Date()).getTime();" title="点击换一张">
<div class="clearfix"></div>
</div>
<div class="form-group">
<button type="submit" id="btnSendMail" class="btn btn-success" style="width: 100%" data-loading-text="正在处理..." autocomplete="off">找回密码</button>
</div>
</form>
</div>
</div>
<div class="clearfix"></div>
</div>
{{template "widgets/footer.tpl" .}}
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="/static/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="/static/layer/layer.js" type="text/javascript"></script>
<script src="/static/js/jquery.form.js" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
$("#email,#code").on('focus',function () {
$(this).tooltip('destroy').parents('.form-group').removeClass('has-error');;
});
$(document).keydown(function (e) {
var event = document.all ? window.event : e;
if(event.keyCode == 13){
$("#btn-login").click();
}
});
$("#findPasswordForm").ajaxForm({
beforeSubmit : function () {
var $btn = $(this).button('loading');
var email = $.trim($("#email").val());
if(email === ""){
$("#email").tooltip({placement:"auto",title : "邮箱不能为空",trigger : 'manual'})
.tooltip('show')
.parents('.form-group').addClass('has-error');
$btn.button('reset');
return false;
}
var code = $.trim($("#code").val());
if(code === ""){
$("#code").tooltip({title : '验证码不能为空',trigger : 'manual'})
.tooltip('show')
.parents('.form-group').addClass('has-error');
$btn.button('reset');
return false;
}
$("#btnSendMail").button("loading");
},
success : function (res) {
if(res.errcode !== 0){
$("#captcha-img").click();
$("#code").val('');
layer.msg(res.message);
$("#btnSendMail").button('reset');
}else{
alert("邮件发送成功,请登录邮箱查看。")
window.location = res.data;
}
},
error :function () {
$("#captcha-img").click();
$("#code").val('');
layer.msg('系统错误');
$("#btnSendMail").button('reset');
}
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="/favicon.ico">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="SmartWiki" />
<title>找回密码 - Powered by MinDoc</title>
<!-- Bootstrap -->
<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link href="/static/css/main.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="/static/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="/static/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="/static/jquery/1.12.4/jquery.min.js"></script>
</head>
<body class="manual-container">
<header class="navbar navbar-static-top smart-nav navbar-fixed-top manual-header" role="banner">
<div class="container">
<div class="navbar-header col-sm-12 col-md-6 col-lg-5">
<a href="/" class="navbar-brand">{{.SITE_NAME}}</a>
</div>
</div>
</header>
<div class="container manual-body">
<div class="row login">
<div class="login-body">
<form role="form" method="post" id="findPasswordForm" action="{{urlfor "AccountController.ValidEmail"}}">
<input type="hidden" name="token" value="{{.Token}}">
<input type="hidden" name="mail" value="{{.Email}}">
<h3 class="text-center">找回密码</h3>
<div class="form-group">
<label for="newPasswd">新密码</label>
<input type="password" class="form-control" name="password1" id="newPassword" maxlength="20" placeholder="新密码" autocomplete="off">
</div>
<div class="form-group">
<label for="configPasswd">确认密码</label>
<input type="password" class="form-control" id="confirmPassword" name="password2" maxlength="20" placeholder="确认密码" autocomplete="off">
</div>
<div class="form-group">
<div class="input-group" style="float: left;width: 195px;">
<div class="input-group-addon">
<i class="fa fa-check-square"></i>
</div>
<input type="text" name="code" id="code" class="form-control" style="width: 150px" maxlength="5" placeholder="验证码" autocomplete="off">&nbsp;
</div>
<img id="captcha-img" style="width: 140px;height: 40px;display: inline-block;float: right" src="{{urlfor "AccountController.Captcha"}}" onclick="this.src='{{urlfor "AccountController.Captcha"}}?key=login&t='+(new Date()).getTime();" title="点击换一张">
<div class="clearfix"></div>
</div>
<div class="form-group">
<button type="submit" id="btnSendMail" class="btn btn-success" style="width: 100%" data-loading-text="正在处理..." autocomplete="off">找回密码</button>
</div>
</form>
</div>
</div>
<div class="clearfix"></div>
</div>
{{template "widgets/footer.tpl" .}}
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="/static/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="/static/layer/layer.js" type="text/javascript"></script>
<script src="/static/js/jquery.form.js" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
$("#email,#code").on('focus',function () {
$(this).tooltip('destroy').parents('.form-group').removeClass('has-error');;
});
$(document).keydown(function (e) {
var event = document.all ? window.event : e;
if(event.keyCode == 13){
$("#btn-login").click();
}
});
$("#findPasswordForm").ajaxForm({
beforeSubmit : function () {
var newPassword = $.trim($("#newPassword").val());
var confirmPassword = $.trim($("#confirmPassword").val());
var code = $.trim($("#code").val());
if(newPassword === ""){
$("#newPassword").tooltip({placement:"auto",title : "密码不能为空",trigger : 'manual'})
.tooltip('show')
.parents('.form-group').addClass('has-error');
return false;
}else if(confirmPassword === ""){
$("#confirmPassword").tooltip({placement:"auto",title : "确认密码不能为空",trigger : 'manual'})
.tooltip('show')
.parents('.form-group').addClass('has-error');
return false;
}else if(newPassword !== confirmPassword) {
$("#confirmPassword").tooltip({placement:"auto",title : "确认密码输入不正确",trigger : 'manual'})
.tooltip('show')
.parents('.form-group').addClass('has-error');
return false;
}else if(code === ""){
$("#code").tooltip({title : '验证码不能为空',trigger : 'manual'})
.tooltip('show')
.parents('.form-group').addClass('has-error');
return false;
}
$("#btnSendMail").button("loading");
},
success : function (res) {
if(res.errcode !== 0){
$("#captcha-img").click();
$("#code").val('');
layer.msg(res.message);
$("#btnSendMail").button('reset');
}else{
window.location = res.data;
}
},
error :function () {
$("#captcha-img").click();
$("#code").val('');
layer.msg('系统错误');
$("#btnSendMail").button('reset');
}
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html>
<head>
<meta name="author" content="SmartWiki" />
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>找回密码 - Powered by MinDoc</title>
<style type="text/css">
.ua-macos::-webkit-scrollbar{ display: none; }
html,body{background-color: transparent;margin:0;padding: 0;}
body{font: 16px/1.5 "Microsoft Yahei", "微软雅黑", verdana;word-wrap:break-word;}
.js-dialog{font-size: 14px;}
pre, .js-pre {
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
font: 16px/1.5 "Microsoft Yahei", "微软雅黑", verdana;
padding:8px 10px;margin:0;
}
.rm_line{border-top:2px solid #F1F1F1; font-size:0; margin:15px 0}
.atchImg img{border:2px solid #c3d9ff;}
.lnkTxt{ color:#0066CC}
.rm_PicArea *{ font-family: "Microsoft Yahei", "微软雅黑", verdana;font-size:16px;font-weight:700;}
.fbk3{ color:#333; line-height:160%}
.fTip{ font-size:11px; font-weight:normal}
img{border:none;vertical-align: middle;}
iframe{display:none;}
*{word-break:break-word;}
#neteaseEncryptedMail{display:none;}
#jy-translate{
position: absolute;
max-width: 500px;
min-width: 100px;
_width:300px;
border: 1px solid rgb(204, 204, 204);
padding: 4px 18px 4px 10px;
background-color: #f9f9f9;
-webkit-border-radius:3px;
-moz-border-radius:3px;
border-radius:3px;
-webkit-box-shadow:#dddddd 0px 0px 10px;
-moz-box-shadow:#dddddd 0px 0px 10px;
box-shadow:#dddddd 0px 0px 10px;
}
#jy-translate h2,
#jy-translate p{color:#555;margin:0;padding:0;}
#jy-translate h2{line-height: 28px;font-size: 14px;}
#jy-translate p{line-height: 24px;font-size: 12px;}
#jy-translate h2 span{font-weight:normal;}
.ua-noyahei,
.ua-noyahei .pre,
.ua-noyahei .js-pre,
.ua-noyahei .rm_PicArea *{font-family: \5b8b\4f53, sans-serif;}
.ua-macos,
.ua-macos .pre,
.ua-macos .js-pre,
.ua-macos .rm_PicArea *{font-family: "Lucida Grande","Hiragino Sans GB","Hiragino Sans GB W3", verdana;}
.jy-contact{float: left;}
.jy-contact-hover{background: #eee;}
.jy-contact img.oprt{width: 23px;height: 23px;border: 0;vertical-align: middle;cursor: pointer;}
</style>
</head>
<body onunload="" class="js-body">
<div>
<div class="wrapper" style="margin: 20px auto 0; width: 500px; padding-top:16px; padding-bottom:10px;">
<div class="header clearfix">
<a class="logo" href="https://www.iminho.me/" target="_blank"><b>MinDoc</b></a>
</div>
<br style="clear:both; height:0">
<div class="content" style="background: none repeat scroll 0 0 #FFFFFF; border: 1px solid #E9E9E9; margin: 2px 0 0; padding: 30px;">
<p>您好: </p>
<p>您在 {{.SITE_NAME}} 提交了找回密码申请。<br>如果您没有提交修改密码的申请, 请忽略本邮件</p>
<p style="border-top: 1px solid #DDDDDD;margin: 15px 0 25px;padding: 15px;">
请点击链接继续: <a href="{{.url}}" target="_blank">{{.url}}</a>
</p>
<p>
好的密码,不但应该容易记住,还要尽量符合以下强度标准:
<ul>
<li>包含大小写字母、数字和符号</li>
<li>不少于 10 位 </li>
<li>不包含生日、手机号码等易被猜出的信息</li>
</ul>
</p>
<p class="footer" style="border-top: 1px solid #DDDDDD; padding-top:6px; margin-top:25px; color:#838383;">
请勿回复本邮件, 此邮箱未受监控, 您不会得到任何回复. 要获得帮助, 请登录网站<br><br>
<a href="https://www.iminho.me/" target="_blank">MinDoc</a>
</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -110,6 +110,7 @@
<div class="article-body {{if eq .Model.Editor "markdown"}}markdown-body editormd-preview-container{{else}}editor-content{{end}}" id="page-content"> <div class="article-body {{if eq .Model.Editor "markdown"}}markdown-body editormd-preview-container{{else}}editor-content{{end}}" id="page-content">
{{.Content}} {{.Content}}
</div> </div>
{{/*
{{if .Model.IsDisplayComment}} {{if .Model.IsDisplayComment}}
<div id="articleComment" class="m-comment"> <div id="articleComment" class="m-comment">
<div class="comment-result"> <div class="comment-result">
@ -154,6 +155,7 @@
</div> </div>
</div> </div>
{{end}} {{end}}
*/}}
</div> </div>
</div> </div>

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="author" content="Minho" />
<link rel="shortcut icon" href="/favicon.ico">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>权限不足 - Powered by MinDoc</title>
<link href="/static/fonts/lato-100.css" rel="stylesheet" type="text/css">
<style type="text/css">
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
color: #B0BEC5;
display: table;
font-weight: 100;
font-family: 'Lato',"Microsoft Yahei","Helvetica Neue",Helvetica,Arial,sans-serif;
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 72px;
margin-bottom: 40px;
color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title">HTTP 403 : 权限不足</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="author" content="Minho" />
<link rel="shortcut icon" href="/favicon.ico">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>页面不存在 - Powered by MinDoc</title>
<link href="/static/fonts/lato-100.css" rel="stylesheet" type="text/css">
<style type="text/css">
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
color: #B0BEC5;
display: table;
font-weight: 100;
font-family: 'Lato',"Microsoft Yahei","Helvetica Neue",Helvetica,Arial,sans-serif;
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 72px;
margin-bottom: 40px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title">HTTP 404 : 页面不存在.</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="author" content="Minho" />
<link rel="shortcut icon" href="/favicon.ico">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>服务器异常 - Powered by MinDoc</title>
<link href="/static/fonts/lato-100.css" rel="stylesheet" type="text/css">
<style type="text/css">
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
color: #B0BEC5;
display: table;
font-weight: 100;
font-family: 'Lato';
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 72px;
margin-bottom: 40px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
{{if .ErrorMessage}}
{{if .ErrorCode}}
<div class="title">HTTP {{.ErrorCode}} {{.ErrorMessage}}</div>
{{else}}
<div class="title">HTTP 500 {{.ErrorMessage}}</div>
{{end}}
{{else}}
<div class="title">HTTP 500 服务器异常</div>
{{end}}
</div>
</div>
</body>
</html>