实现找回密码功能

This commit is contained in:
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
*.prof
.idea
/conf/app.conf

View File

@ -26,7 +26,7 @@ func RegisterDataBase() {
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)
@ -47,22 +47,23 @@ func RegisterModel() {
new(models.Attachment),
new(models.Logger),
new(models.CommentVote),
new(models.MemberToken),
)
}
func Initialization() {
o := orm.NewOrm()
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.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.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.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.NewCommentVote().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
//o := orm.NewOrm()
//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.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.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.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.NewCommentVote().TableNameWithPrefix()+" convert to character set utf8mb4_general_ci;").Exec()
options := []models.Option {
{ OptionName: "ENABLED_CAPTCHA", OptionValue: "false", OptionTitle:"是否启用验证码"},
@ -91,7 +92,6 @@ func RegisterLogger() {
logs.EnableFuncCallDepth(true)
logs.Async()
//beego.BeeLogger.DelLogger("console")
if _,err := os.Stat("logs/log.log"); os.IsNotExist(err) {
if f,err := os.Create("logs/log.log");err == nil {
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

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

38
conf/mail.go Normal file
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 (
"time"
"strings"
"regexp"
"net/smtp"
"github.com/lifei6671/godoc/conf"
"github.com/lifei6671/godoc/models"
"github.com/lifei6671/godoc/utils"
"github.com/astaxie/beego"
"github.com/astaxie/beego/logs"
"github.com/lifei6671/gocaptcha"
"regexp"
"strconv"
)
// AccountController 用户登录与注册.
@ -30,10 +31,7 @@ func (c *AccountController) Login() {
if cookie,ok := c.GetSecureCookie(conf.GetAppKey(),"login");ok{
if err := utils.Decode(cookie,&remember); err == nil {
member := models.NewMember()
member.MemberId = remember.MemberId
if err := models.NewMember().Find(remember.MemberId); err == nil {
if member,err := models.NewMember().Find(remember.MemberId); err == nil {
c.SetMember(*member)
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 退出登录.
func (c *AccountController) Logout(){
c.SetMember(models.Member{});

View File

@ -8,6 +8,8 @@ import (
"github.com/lifei6671/godoc/conf"
"github.com/astaxie/beego"
"strings"
"encoding/json"
"io"
)
@ -62,17 +64,25 @@ func (c *BaseController) SetMember(member models.Member) {
// JsonResult 响应 json 结果
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
json["message"] = errMsg
jsonData["errcode"] = errCode
jsonData["message"] = errMsg
if len(data) > 0 && data[0] != nil{
json["data"] = data[0]
jsonData["data"] = data[0]
}
c.Data["json"] = json
c.ServeJSON(true)
returnJSON, err := json.Marshal(jsonData)
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()
}

View File

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

17
controllers/error.go Normal file
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()
if err := member.Find(member_id); err != nil {
if _,err := member.Find(member_id); err != nil {
c.JsonResult(6002,"用户不存在")
}
member.Status = status
@ -168,7 +168,7 @@ func (c *ManagerController) ChangeMemberRole() {
}
member := models.NewMember()
if err := member.Find(member_id); err != nil {
if _,err := member.Find(member_id); err != nil {
c.JsonResult(6002,"用户不存在")
}
member.Role = role

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,9 @@ import (
"github.com/lifei6671/godoc/utils"
"github.com/lifei6671/godoc/conf"
"github.com/astaxie/beego/logs"
"errors"
"regexp"
"strings"
)
type Member struct {
@ -14,7 +17,7 @@ type Member struct {
Account string `orm:"size(100);unique;column(account)" json:"account"`
Password string `orm:"size(1000);column(password)" json:"-"`
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"`
Avatar string `orm:"size(1000);column(avatar)" json:"avatar"`
//用户角色0 超级管理员 /1 管理员/ 2 普通用户 .
@ -71,6 +74,22 @@ func (m *Member) Login(account string,password string) (*Member,error) {
func (m *Member) Add () (error) {
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);
if err != nil {
@ -92,21 +111,24 @@ func (m *Member) Add () (error) {
func (m *Member) Update(cols... string) (error) {
o := orm.NewOrm()
if m.Email == "" {
return errors.New("邮箱不能为空")
}
if _,err := o.Update(m,cols...);err != nil {
return err
}
return nil
}
func (m *Member) Find(id int) error{
func (m *Member) Find(id int) (*Member,error){
o := orm.NewOrm()
m.MemberId = id
if err := o.Read(m); err != nil {
return err
return m,err
}
m.ResolveRoleName()
return nil
return m,nil
}
func (m *Member) ResolveRoleName (){
@ -163,7 +185,13 @@ func (c *Member) IsAdministrator() bool {
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
}

66
models/member_token.go Normal file
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("/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("/register", &controllers.AccountController{},"*:Register")
beego.Router("/find_password", &controllers.AccountController{},"*:FindPassword")
beego.Router("/valid_email", &controllers.AccountController{},"post:ValidEmail")
beego.Router("/captcha", &controllers.AccountController{},"*:Captcha")
beego.Router("/manager", &controllers.ManagerController{},"*:Index")

Binary file not shown.

BIN
static/fonts/BigBlocko.ttf Normal file

Binary file not shown.

BIN
static/fonts/Bitsumishi.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
static/fonts/Pointy.ttf Normal file

Binary file not shown.

Binary file not shown.

16
static/fonts/lato-100.css Normal file
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;
}

Binary file not shown.

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">
{{.Content}}
</div>
{{/*
{{if .Model.IsDisplayComment}}
<div id="articleComment" class="m-comment">
<div class="comment-result">
@ -154,6 +155,7 @@
</div>
</div>
{{end}}
*/}}
</div>
</div>

52
views/errors/403.tpl Normal file
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>

51
views/errors/404.tpl Normal file
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>

59
views/errors/error.tpl Normal file
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>