Added support for password resets.
Sha256 hashes are now stored in the SFile structures, this will come of use later. Rows should be properly closed in DefaultTopicStore.BulkGetMap. All errors should be properly reported now in DefaultTopicStore.BulkGetMap. Rows should be properly closed in DefaultUserStore.BulkGetMap. All errors should be properly reported now in DefaultUserStore.BulkGetMap. Don't have an account on the login page should now be linkified. Renamed tempra-simple to tempra_simple to avoid breaking the template transpiler. Fixed up bits and pieces of login.html on every theme. Removed an old commented code chunk from template_init.go widget_wol widgets should now get minified. bindToAlerts() should now unbind the alert items before attempting to bind to them. Tweaked the SendValidationEmail phrase. Removed a layer of indentation from DefaultAuth.ValidateMFAToken and added the ErrNoMFAToken error for when MFA isn't setup on the specified account. Email validation now uses a constant time compare to mitigate certain classes of timing attacks. Added the /accounts/password-reset/ route. Added the /accounts/password-reset/submit/ route. Added the /accounts/password-reset/token/ route. Added the /accounts/password-reset/token/submit/ route. Added the password_resets table. Added the password_reset_email_fail phrase. Added the password_reset phrase. Added the password_reset_token phrase. Added the password_reset_email_sent phrase. Added the password_reset_token_token_verified phrase. Added the login_forgot_password phrase. Added the password_reset_head phrase. Added the password_reset_username phrase. Added the password_reset_button phrase. Added the password_reset_subject phrase. Added the password_reset_body phrase. Added the password_reset_token_head phrase. Added the password_reset_token_password phrase. Added the password_reset_token_confirm_password phrase. Added the password_reset_mfa_token phrase. Added the password_reset_token_button phrase. You will need to run the updater or patcher for this commit.
This commit is contained in:
parent
93b292acc0
commit
e22ddfec40
|
@ -166,17 +166,15 @@ func createTables(adapter qgen.Adapter) error {
|
|||
*/
|
||||
|
||||
// TODO: Implement password resets
|
||||
/*qgen.Install.CreateTable("password_resets", "", "",
|
||||
qgen.Install.CreateTable("password_resets", "", "",
|
||||
[]tblColumn{
|
||||
tblColumn{"email", "varchar", 200, false, false, ""},
|
||||
tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
|
||||
tblColumn{"validated", "varchar", 200, false, false, ""}, // Token given once the one-use token is consumed, used to prevent multiple people consuming the same one-use token
|
||||
tblColumn{"token", "varchar", 200, false, false, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"email", "unique"},
|
||||
},
|
||||
)*/
|
||||
tblColumn{"createdAt", "createdAt", 0, false, false, ""},
|
||||
}, nil,
|
||||
)
|
||||
|
||||
qgen.Install.CreateTable("forums", mysqlPre, mysqlCol,
|
||||
[]tblColumn{
|
||||
|
|
|
@ -16,8 +16,9 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Azareal/Gosora/query_gen"
|
||||
"github.com/Azareal/Gosora/common/gauth"
|
||||
"github.com/Azareal/Gosora/query_gen"
|
||||
|
||||
//"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
@ -40,6 +41,7 @@ var ErrPasswordTooLong = errors.New("The password you selected is too long")
|
|||
var ErrWrongPassword = errors.New("That's not the correct password.")
|
||||
var ErrBadMFAToken = errors.New("I'm not sure where you got that from, but that's not a valid 2FA token")
|
||||
var ErrWrongMFAToken = errors.New("That 2FA token isn't correct")
|
||||
var ErrNoMFAToken = errors.New("This user doesn't have 2FA setup")
|
||||
var ErrSecretError = errors.New("There was a glitch in the system. Please contact your local administrator.")
|
||||
var ErrNoUserByName = errors.New("We couldn't find an account with that username.")
|
||||
var DefaultHashAlgo = "bcrypt" // Override this in the configuration file, not here
|
||||
|
@ -132,7 +134,10 @@ func (auth *DefaultAuth) ValidateMFAToken(mfaToken string, uid int) error {
|
|||
LogError(err)
|
||||
return ErrSecretError
|
||||
}
|
||||
if err != ErrNoRows {
|
||||
if err == ErrNoRows {
|
||||
return ErrNoMFAToken
|
||||
}
|
||||
|
||||
ok, err := VerifyGAuthToken(mfaItem.Secret, mfaToken)
|
||||
if err != nil {
|
||||
return ErrBadMFAToken
|
||||
|
@ -150,7 +155,6 @@ func (auth *DefaultAuth) ValidateMFAToken(mfaToken string, uid int) error {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return ErrWrongMFAToken
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ func SendValidationEmail(username string, email string, token string) error {
|
|||
|
||||
// TODO: Move these to the phrase system
|
||||
subject := "Validate Your Email - " + Site.Name
|
||||
msg := "Dear " + username + ", following your registration on our forums, we ask you to validate your email, so that we can confirm that this email actually belongs to you.\n\nClick on the following link to do so. " + schema + "://" + Site.URL + "/user/edit/token/" + token + "\n\nIf you haven't created an account here, then please feel free to ignore this email.\nWe're sorry for the inconvenience this may have caused."
|
||||
msg := "Dear " + username + ", to complete your registration on our forums, we need you to validate your email, so that we can confirm that this email actually belongs to you.\n\nClick on the following link to do so. " + schema + "://" + Site.URL + "/user/edit/token/" + token + "\n\nIf you haven't created an account here, then please feel free to ignore this email.\nWe're sorry for the inconvenience this may have caused."
|
||||
return SendEmail(email, subject, msg)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ package common
|
|||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -25,6 +27,7 @@ var staticFileMutex sync.RWMutex
|
|||
type SFile struct {
|
||||
Data []byte
|
||||
GzipData []byte
|
||||
Sha256 []byte
|
||||
Pos int64
|
||||
Length int64
|
||||
GzipLength int64
|
||||
|
@ -234,7 +237,12 @@ func (list SFileList) JSTmplInit() error {
|
|||
return err
|
||||
}
|
||||
|
||||
list.Set("/static/"+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
// Get a checksum for CSPs and cache busting
|
||||
hasher := sha256.New()
|
||||
hasher.Write(data)
|
||||
checksum := []byte(hex.EncodeToString(hasher.Sum(nil)))
|
||||
|
||||
list.Set("/static/"+path, SFile{data, gzipData, checksum, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
|
||||
DebugLogf("Added the '%s' static file.", path)
|
||||
return nil
|
||||
|
@ -256,6 +264,11 @@ func (list SFileList) Init() error {
|
|||
var ext = filepath.Ext("/public/" + path)
|
||||
mimetype := mime.TypeByExtension(ext)
|
||||
|
||||
// Get a checksum for CSPs and cache busting
|
||||
hasher := sha256.New()
|
||||
hasher.Write(data)
|
||||
checksum := []byte(hex.EncodeToString(hasher.Sum(nil)))
|
||||
|
||||
// Avoid double-compressing images
|
||||
var gzipData []byte
|
||||
if mimetype != "image/jpeg" && mimetype != "image/png" && mimetype != "image/gif" {
|
||||
|
@ -274,7 +287,7 @@ func (list SFileList) Init() error {
|
|||
}
|
||||
}
|
||||
|
||||
list.Set("/static/"+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mimetype, f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
list.Set("/static/"+path, SFile{data, gzipData, checksum, 0, int64(len(data)), int64(len(gzipData)), mimetype, f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
|
||||
DebugLogf("Added the '%s' static file.", path)
|
||||
return nil
|
||||
|
@ -302,7 +315,12 @@ func (list SFileList) Add(path string, prefix string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
list.Set("/static"+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
// Get a checksum for CSPs and cache busting
|
||||
hasher := sha256.New()
|
||||
hasher.Write(data)
|
||||
checksum := []byte(hex.EncodeToString(hasher.Sum(nil)))
|
||||
|
||||
list.Set("/static"+path, SFile{data, gzipData, checksum, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
|
||||
DebugLogf("Added the '%s' static file", path)
|
||||
return nil
|
||||
|
|
|
@ -203,6 +203,13 @@ type LevelListPage struct {
|
|||
Levels []LevelListItem
|
||||
}
|
||||
|
||||
type ResetPage struct {
|
||||
*Header
|
||||
UID int
|
||||
Token string
|
||||
MFA bool
|
||||
}
|
||||
|
||||
type PanelStats struct {
|
||||
Users int
|
||||
Groups int
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/Azareal/Gosora/query_gen"
|
||||
)
|
||||
|
||||
var PasswordResetter *DefaultPasswordResetter
|
||||
var ErrBadResetToken = errors.New("This reset token has expired.")
|
||||
|
||||
type DefaultPasswordResetter struct {
|
||||
getTokens *sql.Stmt
|
||||
create *sql.Stmt
|
||||
delete *sql.Stmt
|
||||
}
|
||||
|
||||
func NewDefaultPasswordResetter(acc *qgen.Accumulator) (*DefaultPasswordResetter, error) {
|
||||
return &DefaultPasswordResetter{
|
||||
getTokens: acc.Select("password_resets").Columns("token").Where("uid = ?").Prepare(),
|
||||
create: acc.Insert("password_resets").Columns("email, uid, validated, token, createdAt").Fields("?,?,0,?,UTC_TIMESTAMP()").Prepare(),
|
||||
delete: acc.Delete("password_resets").Where("uid =?").Prepare(),
|
||||
}, acc.FirstError()
|
||||
}
|
||||
|
||||
func (r *DefaultPasswordResetter) Create(email string, uid int, token string) error {
|
||||
_, err := r.create.Exec(email, uid, token)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *DefaultPasswordResetter) FlushTokens(uid int) error {
|
||||
_, err := r.delete.Exec(uid)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *DefaultPasswordResetter) ValidateToken(uid int, token string) error {
|
||||
rows, err := r.getTokens.Query(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var success = false
|
||||
for rows.Next() {
|
||||
var rtoken string
|
||||
err := rows.Scan(&rtoken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(rtoken)) == 1 {
|
||||
success = true
|
||||
}
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !success {
|
||||
return ErrBadResetToken
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -508,119 +508,12 @@ func compileJSTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName stri
|
|||
}
|
||||
writeTemplate("alert", alertTmpl)
|
||||
/*//writeTemplate("forum", forumTmpl)
|
||||
writeTemplate("topics_topic", topicListItemTmpl)
|
||||
writeTemplate("topic_posts", topicPostsTmpl)
|
||||
writeTemplate("topic_alt_posts", topicAltPostsTmpl)
|
||||
writeTemplate("paginator", paginatorTmpl)
|
||||
//writeTemplate("panel_themes_widgets_widget", panelWidgetsWidgetTmpl)
|
||||
writeTemplateList(c, &wg, dirPrefix)*/
|
||||
return nil
|
||||
}
|
||||
|
||||
/*func CompileJSTemplates() error {
|
||||
log.Print("Compiling the JS templates")
|
||||
var config tmpl.CTemplateConfig
|
||||
config.Minify = Config.MinifyTemplates
|
||||
config.Debug = Dev.DebugMode
|
||||
config.SuperDebug = Dev.TemplateDebug
|
||||
config.SkipHandles = true
|
||||
config.SkipTmplPtrMap = true
|
||||
config.SkipInitBlock = false
|
||||
config.PackageName = "tmpl"
|
||||
|
||||
c := tmpl.NewCTemplateSet()
|
||||
c.SetConfig(config)
|
||||
c.SetBaseImportMap(map[string]string{
|
||||
"io": "io",
|
||||
"github.com/Azareal/Gosora/common/alerts": "github.com/Azareal/Gosora/common/alerts",
|
||||
})
|
||||
c.SetBuildTags("!no_templategen")
|
||||
|
||||
user, user2, user3 := tmplInitUsers()
|
||||
header, _, _ := tmplInitHeaders(user, user2, user3)
|
||||
now := time.Now()
|
||||
var varList = make(map[string]tmpl.VarItem)
|
||||
|
||||
// TODO: Check what sort of path is sent exactly and use it here
|
||||
alertItem := alerts.AlertItem{Avatar: "", ASID: 1, Path: "/", Message: "uh oh, something happened"}
|
||||
alertTmpl, err := c.Compile("alert.html", "templates/", "alerts.AlertItem", alertItem, varList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.SetBaseImportMap(map[string]string{
|
||||
"io": "io",
|
||||
"github.com/Azareal/Gosora/common": "github.com/Azareal/Gosora/common",
|
||||
})
|
||||
// TODO: Fix the import loop so we don't have to use this hack anymore
|
||||
c.SetBuildTags("!no_templategen,tmplgentopic")
|
||||
|
||||
var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}
|
||||
topicListItemTmpl, err := c.Compile("topics_topic.html", "templates/", "*common.TopicsRow", topicsRow, varList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
poll := Poll{ID: 1, Type: 0, Options: map[int]string{0: "Nothing", 1: "Something"}, Results: map[int]int{0: 5, 1: 2}, QuickOptions: []PollOption{
|
||||
PollOption{0, "Nothing"},
|
||||
PollOption{1, "Something"},
|
||||
}, VoteCount: 7}
|
||||
avatar, microAvatar := BuildAvatar(62, "")
|
||||
miniAttach := []*MiniAttachment{&MiniAttachment{Path: "/"}}
|
||||
topic := TopicUser{1, "blah", "Blah", "Hey there!", 62, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach}
|
||||
var replyList []ReplyUser
|
||||
// TODO: Do we really want the UID here to be zero?
|
||||
avatar, microAvatar = BuildAvatar(0, "")
|
||||
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, 1, "", "", miniAttach})
|
||||
|
||||
varList = make(map[string]tmpl.VarItem)
|
||||
header.Title = "Topic Name"
|
||||
tpage := TopicPage{header, replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, poll, Paginator{[]int{1}, 1, 1}}
|
||||
tpage.Forum.Link = BuildForumURL(NameToSlug(tpage.Forum.Name), tpage.Forum.ID)
|
||||
topicPostsTmpl, err := c.Compile("topic_posts.html", "templates/", "common.TopicPage", tpage, varList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
topicAltPostsTmpl, err := c.Compile("topic_alt_posts.html", "templates/", "common.TopicPage", tpage, varList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
itemsPerPage := 25
|
||||
_, page, lastPage := PageOffset(20, 1, itemsPerPage)
|
||||
pageList := Paginate(20, itemsPerPage, 5)
|
||||
paginatorTmpl, err := c.Compile("paginator.html", "templates/", "common.Paginator", Paginator{pageList, page, lastPage}, varList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dirPrefix = "./tmpl_client/"
|
||||
var wg sync.WaitGroup
|
||||
var writeTemplate = func(name string, content string) {
|
||||
log.Print("Writing template '" + name + "'")
|
||||
if content == "" {
|
||||
return //log.Fatal("No content body")
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err := writeFile(dirPrefix+"template_"+name+".go", content)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
writeTemplate("alert", alertTmpl)
|
||||
//writeTemplate("forum", forumTmpl)
|
||||
writeTemplate("topics_topic", topicListItemTmpl)
|
||||
writeTemplate("topic_posts", topicPostsTmpl)
|
||||
writeTemplate("topic_alt_posts", topicAltPostsTmpl)
|
||||
writeTemplate("paginator", paginatorTmpl)
|
||||
//writeTemplate("panel_themes_widgets_widget", panelWidgetsWidgetTmpl)
|
||||
writeTemplateList(c, &wg, dirPrefix)
|
||||
return nil
|
||||
}*/
|
||||
|
||||
func getTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string) string {
|
||||
DebugLog("in getTemplateList")
|
||||
pout := "\n// nolint\nfunc init() {\n"
|
||||
|
|
|
@ -3,7 +3,9 @@ package common
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
htmpl "html/template"
|
||||
"io"
|
||||
|
@ -157,7 +159,12 @@ func (theme *Theme) AddThemeStaticFiles() error {
|
|||
return err
|
||||
}
|
||||
|
||||
StaticFiles.Set("/static/"+theme.Name+path, SFile{data, gzipData, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
// Get a checksum for CSPs and cache busting
|
||||
hasher := sha256.New()
|
||||
hasher.Write(data)
|
||||
checksum := []byte(hex.EncodeToString(hasher.Sum(nil)))
|
||||
|
||||
StaticFiles.Set("/static/"+theme.Name+path, SFile{data, gzipData, checksum, 0, int64(len(data)), int64(len(gzipData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
|
||||
DebugLog("Added the '/" + theme.Name + path + "' static file for theme " + theme.Name + ".")
|
||||
return nil
|
||||
|
|
|
@ -152,6 +152,8 @@ func (s *DefaultTopicStore) BulkGetMap(ids []int) (list map[int]*Topic, err erro
|
|||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
topic := &Topic{}
|
||||
err := rows.Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyBy, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data)
|
||||
|
@ -162,6 +164,10 @@ func (s *DefaultTopicStore) BulkGetMap(ids []int) (list map[int]*Topic, err erro
|
|||
s.cache.Set(topic)
|
||||
list[topic.ID] = topic
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
// Did we miss any topics?
|
||||
if idCount > len(list) {
|
||||
|
|
|
@ -186,6 +186,8 @@ func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err erro
|
|||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
user := &User{Loggedin: true}
|
||||
err := rows.Scan(&user.ID, &user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.RawAvatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Liked, &user.LastIP, &user.TempGroup)
|
||||
|
@ -196,6 +198,10 @@ func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err erro
|
|||
mus.cache.Set(user)
|
||||
list[user.ID] = user
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
// Did we miss any users?
|
||||
if idCount > len(list) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net/http/httptest"
|
||||
|
||||
"github.com/Azareal/Gosora/common/phrases"
|
||||
min "github.com/Azareal/Gosora/common/templates"
|
||||
)
|
||||
|
||||
type wolUsers struct {
|
||||
|
@ -53,6 +54,10 @@ func wolTick(widget *Widget) error {
|
|||
}
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(w.Result().Body)
|
||||
widget.TickMask.Store(buf.String())
|
||||
bs := buf.String()
|
||||
if Config.MinifyTemplates {
|
||||
bs = min.Minify(bs)
|
||||
}
|
||||
widget.TickMask.Store(bs)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -154,6 +154,10 @@ var RouteMap = map[string]interface{}{
|
|||
"routes.AccountLoginMFAVerify": routes.AccountLoginMFAVerify,
|
||||
"routes.AccountLoginMFAVerifySubmit": routes.AccountLoginMFAVerifySubmit,
|
||||
"routes.AccountRegisterSubmit": routes.AccountRegisterSubmit,
|
||||
"routes.AccountPasswordReset": routes.AccountPasswordReset,
|
||||
"routes.AccountPasswordResetSubmit": routes.AccountPasswordResetSubmit,
|
||||
"routes.AccountPasswordResetToken": routes.AccountPasswordResetToken,
|
||||
"routes.AccountPasswordResetTokenSubmit": routes.AccountPasswordResetTokenSubmit,
|
||||
"routes.DynamicRoute": routes.DynamicRoute,
|
||||
"routes.UploadedFile": routes.UploadedFile,
|
||||
"routes.StaticFile": routes.StaticFile,
|
||||
|
@ -295,12 +299,16 @@ var routeMapEnum = map[string]int{
|
|||
"routes.AccountLoginMFAVerify": 128,
|
||||
"routes.AccountLoginMFAVerifySubmit": 129,
|
||||
"routes.AccountRegisterSubmit": 130,
|
||||
"routes.DynamicRoute": 131,
|
||||
"routes.UploadedFile": 132,
|
||||
"routes.StaticFile": 133,
|
||||
"routes.RobotsTxt": 134,
|
||||
"routes.SitemapXml": 135,
|
||||
"routes.BadRoute": 136,
|
||||
"routes.AccountPasswordReset": 131,
|
||||
"routes.AccountPasswordResetSubmit": 132,
|
||||
"routes.AccountPasswordResetToken": 133,
|
||||
"routes.AccountPasswordResetTokenSubmit": 134,
|
||||
"routes.DynamicRoute": 135,
|
||||
"routes.UploadedFile": 136,
|
||||
"routes.StaticFile": 137,
|
||||
"routes.RobotsTxt": 138,
|
||||
"routes.SitemapXml": 139,
|
||||
"routes.BadRoute": 140,
|
||||
}
|
||||
var reverseRouteMapEnum = map[int]string{
|
||||
0: "routes.Overview",
|
||||
|
@ -434,12 +442,16 @@ var reverseRouteMapEnum = map[int]string{
|
|||
128: "routes.AccountLoginMFAVerify",
|
||||
129: "routes.AccountLoginMFAVerifySubmit",
|
||||
130: "routes.AccountRegisterSubmit",
|
||||
131: "routes.DynamicRoute",
|
||||
132: "routes.UploadedFile",
|
||||
133: "routes.StaticFile",
|
||||
134: "routes.RobotsTxt",
|
||||
135: "routes.SitemapXml",
|
||||
136: "routes.BadRoute",
|
||||
131: "routes.AccountPasswordReset",
|
||||
132: "routes.AccountPasswordResetSubmit",
|
||||
133: "routes.AccountPasswordResetToken",
|
||||
134: "routes.AccountPasswordResetTokenSubmit",
|
||||
135: "routes.DynamicRoute",
|
||||
136: "routes.UploadedFile",
|
||||
137: "routes.StaticFile",
|
||||
138: "routes.RobotsTxt",
|
||||
139: "routes.SitemapXml",
|
||||
140: "routes.BadRoute",
|
||||
}
|
||||
var osMapEnum = map[string]int{
|
||||
"unknown": 0,
|
||||
|
@ -738,7 +750,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
counters.GlobalViewCounter.Bump()
|
||||
|
||||
if prefix == "/static" {
|
||||
counters.RouteViewCounter.Bump(133)
|
||||
counters.RouteViewCounter.Bump(137)
|
||||
req.URL.Path += extraData
|
||||
routes.StaticFile(w, req)
|
||||
return
|
||||
|
@ -2085,6 +2097,36 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
|
||||
counters.RouteViewCounter.Bump(130)
|
||||
err = routes.AccountRegisterSubmit(w,req,user)
|
||||
case "/accounts/password-reset/":
|
||||
counters.RouteViewCounter.Bump(131)
|
||||
head, err := common.UserCheck(w,req,&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = routes.AccountPasswordReset(w,req,user,head)
|
||||
case "/accounts/password-reset/submit/":
|
||||
err = common.ParseForm(w,req,user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(132)
|
||||
err = routes.AccountPasswordResetSubmit(w,req,user)
|
||||
case "/accounts/password-reset/token/":
|
||||
counters.RouteViewCounter.Bump(133)
|
||||
head, err := common.UserCheck(w,req,&user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = routes.AccountPasswordResetToken(w,req,user,head)
|
||||
case "/accounts/password-reset/token/submit/":
|
||||
err = common.ParseForm(w,req,user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
counters.RouteViewCounter.Bump(134)
|
||||
err = routes.AccountPasswordResetTokenSubmit(w,req,user)
|
||||
}
|
||||
/*case "/sitemaps": // TODO: Count these views
|
||||
req.URL.Path += extraData
|
||||
|
@ -2099,7 +2141,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
w.Header().Del("Content-Type")
|
||||
w.Header().Del("Content-Encoding")
|
||||
}
|
||||
counters.RouteViewCounter.Bump(132)
|
||||
counters.RouteViewCounter.Bump(136)
|
||||
req.URL.Path += extraData
|
||||
// TODO: Find a way to propagate errors up from this?
|
||||
r.UploadHandler(w,req) // TODO: Count these views
|
||||
|
@ -2109,7 +2151,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
// TODO: Add support for favicons and robots.txt files
|
||||
switch(extraData) {
|
||||
case "robots.txt":
|
||||
counters.RouteViewCounter.Bump(134)
|
||||
counters.RouteViewCounter.Bump(138)
|
||||
return routes.RobotsTxt(w,req)
|
||||
case "favicon.ico":
|
||||
req.URL.Path = "/static"+req.URL.Path+extraData
|
||||
|
@ -2117,7 +2159,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
routes.StaticFile(w,req)
|
||||
return nil
|
||||
/*case "sitemap.xml":
|
||||
counters.RouteViewCounter.Bump(135)
|
||||
counters.RouteViewCounter.Bump(139)
|
||||
return routes.SitemapXml(w,req)*/
|
||||
}
|
||||
return common.NotFound(w,req,nil)
|
||||
|
@ -2128,7 +2170,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
r.RUnlock()
|
||||
|
||||
if ok {
|
||||
counters.RouteViewCounter.Bump(131) // TODO: Be more specific about *which* dynamic route it is
|
||||
counters.RouteViewCounter.Bump(135) // TODO: Be more specific about *which* dynamic route it is
|
||||
req.URL.Path += extraData
|
||||
return handle(w,req,user)
|
||||
}
|
||||
|
@ -2139,7 +2181,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
|||
} else {
|
||||
r.DumpRequest(req,"Bad Route")
|
||||
}
|
||||
counters.RouteViewCounter.Bump(136)
|
||||
counters.RouteViewCounter.Bump(140)
|
||||
return common.NotFound(w,req,nil)
|
||||
}
|
||||
return err
|
||||
|
|
|
@ -100,6 +100,8 @@
|
|||
"register_username_too_long_prefix":"The username is too long, max: ",
|
||||
"register_email_fail":"We were unable to send the email for you to confirm that this email address belongs to you. You may not have access to some functionality until you do so. Please ask an administrator for assistance.",
|
||||
|
||||
"password_reset_email_fail":"We were unable to send a password reset email to this user.",
|
||||
|
||||
"alerts_no_actor":"Unable to find the actor",
|
||||
"alerts_no_target_user":"Unable to find the target user",
|
||||
"alerts_no_linked_topic":"Unable to find the linked topic",
|
||||
|
@ -128,6 +130,8 @@
|
|||
"login":"Login",
|
||||
"login_mfa_verify":"2FA Verify",
|
||||
"register":"Registration",
|
||||
"password_reset":"Password Reset",
|
||||
"password_reset_token":"Password Reset",
|
||||
"ip_search":"IP Search",
|
||||
"profile": "%s's Profile",
|
||||
"account":"My Account",
|
||||
|
@ -305,6 +309,8 @@
|
|||
"account_mail_disabled":"The mail system is currently disabled.",
|
||||
"account_mail_verify_success":"Your email was successfully verified.",
|
||||
"account_mfa_setup_success":"Two-factor authentication was successfully setup for your account.",
|
||||
"password_reset_email_sent":"An email was sent to you. Please follow the steps within.",
|
||||
"password_reset_token_token_verified":"Your password was successfully updated.",
|
||||
|
||||
"panel_forum_created":"The forum was successfully created.",
|
||||
"panel_forum_deleted":"The forum was successfully deleted.",
|
||||
|
@ -446,6 +452,7 @@
|
|||
"login_account_password":"Password",
|
||||
"login_submit_button":"Login",
|
||||
"login_no_account":"Don't have an account?",
|
||||
"login_forgot_password":"Forgot your password?",
|
||||
|
||||
"login_mfa_verify_head":"2FA Verify",
|
||||
"login_mfa_verify_explanation":"Please input the code from the authenticator app below.",
|
||||
|
@ -460,6 +467,18 @@
|
|||
"register_account_anti_spam":"Are you a spambot?",
|
||||
"register_submit_button":"Create Account",
|
||||
|
||||
"password_reset_head":"Password Reset",
|
||||
"password_reset_username":"Account Name",
|
||||
"password_reset_button":"Send Email",
|
||||
"password_reset_subject":"Reset your email",
|
||||
"password_reset_body":"Dear %s, someone has requested that your password be reset. If this was you, then please click on the following link to do so, otherwise disregard this email.\n\n %s",
|
||||
|
||||
"password_reset_token_head":"Password Reset",
|
||||
"password_reset_token_password":"New Password",
|
||||
"password_reset_token_confirm_password":"Confirm Password",
|
||||
"password_reset_mfa_token":"2FA Token",
|
||||
"password_reset_token_button":"Update Account",
|
||||
|
||||
"account_menu_head":"My Account",
|
||||
"account_menu_password":"Password",
|
||||
"account_menu_email":"Email",
|
||||
|
|
4
main.go
4
main.go
|
@ -165,6 +165,10 @@ func afterDBInit() (err error) {
|
|||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
common.PasswordResetter, err = common.NewDefaultPasswordResetter(acc)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
// TODO: Let the admin choose other thumbnailers, maybe ones defined in plugins
|
||||
common.Thumbnailer = common.NewCaireThumbnailer()
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ func init() {
|
|||
addPatch(13, patch13)
|
||||
addPatch(14, patch14)
|
||||
addPatch(15, patch15)
|
||||
addPatch(16, patch16)
|
||||
}
|
||||
|
||||
func patch0(scanner *bufio.Scanner) (err error) {
|
||||
|
@ -537,3 +538,15 @@ func patch14(scanner *bufio.Scanner) error {
|
|||
func patch15(scanner *bufio.Scanner) error {
|
||||
return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'google_site_verify','','html-attribute'"))
|
||||
}
|
||||
|
||||
func patch16(scanner *bufio.Scanner) error {
|
||||
return execStmt(qgen.Builder.CreateTable("password_resets", "", "",
|
||||
[]tblColumn{
|
||||
tblColumn{"email", "varchar", 200, false, false, ""},
|
||||
tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
|
||||
tblColumn{"validated", "varchar", 200, false, false, ""}, // Token given once the one-use token is consumed, used to prevent multiple people consuming the same one-use token
|
||||
tblColumn{"token", "varchar", 200, false, false, ""},
|
||||
tblColumn{"createdAt", "createdAt", 0, false, false, ""},
|
||||
}, nil,
|
||||
))
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ function postLink(event) {
|
|||
}
|
||||
|
||||
function bindToAlerts() {
|
||||
$(".alertItem.withAvatar a").unbind("click");
|
||||
$(".alertItem.withAvatar a").click(function(event) {
|
||||
event.stopPropagation();
|
||||
$.ajax({ url: "/api/?action=set&module=dismiss-alert", type: "POST", dataType: "json", error: ajaxError, data: { asid: $(this).attr("data-asid") } });
|
||||
|
|
|
@ -65,6 +65,7 @@ func userRoutes() *RouteGroup {
|
|||
|
||||
MemberView("routes.LevelList", "/user/levels/"),
|
||||
//MemberView("routes.LevelRankings", "/user/rankings/"),
|
||||
//MemberView("routes.Alerts", "/user/alerts/"),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -135,8 +136,11 @@ func accountRoutes() *RouteGroup {
|
|||
View("routes.AccountLoginMFAVerify", "/accounts/mfa_verify/"),
|
||||
AnonAction("routes.AccountLoginMFAVerifySubmit", "/accounts/mfa_verify/submit/"), // We have logic in here which filters out regular guests
|
||||
AnonAction("routes.AccountRegisterSubmit", "/accounts/create/submit/"),
|
||||
//View("routes.AccountPasswordReset", "/accounts/password-reset/"),
|
||||
//AnonAction("routes.AccountPasswordResetSubmit", "/accounts/password-reset/submit/"),
|
||||
|
||||
View("routes.AccountPasswordReset", "/accounts/password-reset/"),
|
||||
AnonAction("routes.AccountPasswordResetSubmit", "/accounts/password-reset/submit/"),
|
||||
View("routes.AccountPasswordResetToken", "/accounts/password-reset/token/"),
|
||||
AnonAction("routes.AccountPasswordResetTokenSubmit", "/accounts/password-reset/token/submit/"),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto/subtle"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
|
@ -424,7 +425,7 @@ func AccountEditPasswordSubmit(w http.ResponseWriter, r *http.Request, user comm
|
|||
if newPassword != confirmPassword {
|
||||
return common.LocalError("The two passwords don't match.", w, r, user)
|
||||
}
|
||||
common.SetPassword(user.ID, newPassword)
|
||||
common.SetPassword(user.ID, newPassword) // TODO: Limited version of WeakPassword()
|
||||
|
||||
// Log the user out as a safety precaution
|
||||
common.Auth.ForceLogout(user.ID)
|
||||
|
@ -693,7 +694,7 @@ func AccountEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, user co
|
|||
return common.LocalError("You are not logged in", w, r, user)
|
||||
}
|
||||
for _, email := range emails {
|
||||
if email.Token == token {
|
||||
if subtle.ConstantTimeCompare([]byte(email.Token), []byte(token)) == 1 {
|
||||
targetEmail = email
|
||||
}
|
||||
}
|
||||
|
@ -761,3 +762,154 @@ func LevelList(w http.ResponseWriter, r *http.Request, user common.User, header
|
|||
pi := common.LevelListPage{header, levels[1:]}
|
||||
return renderTemplate("level_list", w, r, header, pi)
|
||||
}
|
||||
|
||||
func Alerts(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header) common.RouteError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func AccountPasswordReset(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header) common.RouteError {
|
||||
if user.Loggedin {
|
||||
return common.LocalError("You're already logged in.", w, r, user)
|
||||
}
|
||||
if !common.Site.EnableEmails {
|
||||
return common.LocalError(phrases.GetNoticePhrase("account_mail_disabled"), w, r, user)
|
||||
}
|
||||
if r.FormValue("email_sent") == "1" {
|
||||
header.AddNotice("password_reset_email_sent")
|
||||
}
|
||||
header.Title = phrases.GetTitlePhrase("password_reset")
|
||||
pi := common.Page{header, tList, nil}
|
||||
return renderTemplate("password_reset", w, r, header, pi)
|
||||
}
|
||||
|
||||
// TODO: Ratelimit this
|
||||
func AccountPasswordResetSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
|
||||
if user.Loggedin {
|
||||
return common.LocalError("You're already logged in.", w, r, user)
|
||||
}
|
||||
if !common.Site.EnableEmails {
|
||||
return common.LocalError(phrases.GetNoticePhrase("account_mail_disabled"), w, r, user)
|
||||
}
|
||||
|
||||
username := r.PostFormValue("username")
|
||||
tuser, err := common.Users.GetByName(username)
|
||||
if err == sql.ErrNoRows {
|
||||
// Someone trying to stir up trouble?
|
||||
http.Redirect(w, r, "/accounts/password-reset/?email_sent=1", http.StatusSeeOther)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
|
||||
token, err := common.GenerateSafeString(80)
|
||||
if err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
|
||||
// TODO: Move this query somewhere else
|
||||
var disc string
|
||||
err = qgen.NewAcc().Select("password_resets").Columns("createdAt").DateCutoff("createdAt", 1, "hour").QueryRow().Scan(&disc)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
if err == nil {
|
||||
return common.LocalError("You can only send a password reset email for a user once an hour", w, r, user)
|
||||
}
|
||||
|
||||
err = common.PasswordResetter.Create(tuser.Email, tuser.ID, token)
|
||||
if err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
|
||||
var schema string
|
||||
if common.Site.EnableSsl {
|
||||
schema = "s"
|
||||
}
|
||||
|
||||
err = common.SendEmail(tuser.Email, phrases.GetTmplPhrase("password_reset_subject"), phrases.GetTmplPhrasef("password_reset_body", tuser.Name, "http"+schema+"://"+common.Site.URL+"/accounts/password-reset/token/?uid="+strconv.Itoa(tuser.ID)+"&token="+token))
|
||||
if err != nil {
|
||||
return common.LocalError(phrases.GetErrorPhrase("password_reset_email_fail"), w, r, user)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/accounts/password-reset/?email_sent=1", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
||||
func AccountPasswordResetToken(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header) common.RouteError {
|
||||
if user.Loggedin {
|
||||
return common.LocalError("You're already logged in.", w, r, user)
|
||||
}
|
||||
// TODO: Find a way to flash this notice
|
||||
/*if r.FormValue("token_verified") == "1" {
|
||||
header.AddNotice("password_reset_token_token_verified")
|
||||
}*/
|
||||
|
||||
token := r.FormValue("token")
|
||||
uid, err := strconv.Atoi(r.FormValue("uid"))
|
||||
if err != nil {
|
||||
return common.LocalError("Invalid uid", w, r, user)
|
||||
}
|
||||
|
||||
err = common.PasswordResetter.ValidateToken(uid, token)
|
||||
if err == sql.ErrNoRows || err == common.ErrBadResetToken {
|
||||
return common.LocalError("This reset token has expired.", w, r, user)
|
||||
} else if err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
|
||||
_, err = common.MFAstore.Get(uid)
|
||||
if err != sql.ErrNoRows && err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
mfa := err != sql.ErrNoRows
|
||||
|
||||
header.Title = phrases.GetTitlePhrase("password_reset_token")
|
||||
return renderTemplate("password_reset_token", w, r, header, common.ResetPage{header, uid, html.EscapeString(token), mfa})
|
||||
}
|
||||
|
||||
func AccountPasswordResetTokenSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
|
||||
if user.Loggedin {
|
||||
return common.LocalError("You're already logged in.", w, r, user)
|
||||
}
|
||||
|
||||
token := r.FormValue("token")
|
||||
uid, err := strconv.Atoi(r.FormValue("uid"))
|
||||
if err != nil {
|
||||
return common.LocalError("Invalid uid", w, r, user)
|
||||
}
|
||||
if !common.Users.Exists(uid) {
|
||||
return common.LocalError("This reset token has expired.", w, r, user)
|
||||
}
|
||||
|
||||
err = common.PasswordResetter.ValidateToken(uid, token)
|
||||
if err == sql.ErrNoRows || err == common.ErrBadResetToken {
|
||||
return common.LocalError("This reset token has expired.", w, r, user)
|
||||
} else if err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
|
||||
mfaToken := r.PostFormValue("mfa_token")
|
||||
err = common.Auth.ValidateMFAToken(mfaToken, uid)
|
||||
if err != nil && err != common.ErrNoMFAToken {
|
||||
return common.LocalError(err.Error(), w, r, user)
|
||||
}
|
||||
|
||||
newPassword := r.PostFormValue("password")
|
||||
confirmPassword := r.PostFormValue("confirm_password")
|
||||
if newPassword != confirmPassword {
|
||||
return common.LocalError("The two passwords don't match.", w, r, user)
|
||||
}
|
||||
common.SetPassword(uid, newPassword) // TODO: Limited version of WeakPassword()
|
||||
|
||||
err = common.PasswordResetter.FlushTokens(uid)
|
||||
if err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
|
||||
// Log the user out as a safety precaution
|
||||
common.Auth.ForceLogout(uid)
|
||||
|
||||
//http.Redirect(w, r, "/accounts/password-reset/token/?token_verified=1", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE [password_resets] (
|
||||
[email] nvarchar (200) not null,
|
||||
[uid] int not null,
|
||||
[validated] nvarchar (200) not null,
|
||||
[token] nvarchar (200) not null,
|
||||
[createdAt] datetime not null
|
||||
);
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE `password_resets` (
|
||||
`email` varchar(200) not null,
|
||||
`uid` int not null,
|
||||
`validated` varchar(200) not null,
|
||||
`token` varchar(200) not null,
|
||||
`createdAt` datetime not null
|
||||
);
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE "password_resets" (
|
||||
`email` varchar (200) not null,
|
||||
`uid` int not null,
|
||||
`validated` varchar (200) not null,
|
||||
`token` varchar (200) not null,
|
||||
`createdAt` timestamp not null
|
||||
);
|
|
@ -15,7 +15,12 @@
|
|||
</div>
|
||||
<div class="formrow login_button_row form_button_row">
|
||||
<div class="formitem"><button name="login-button" class="formbutton">{{lang "login_submit_button"}}</button></div>
|
||||
<div class="formitem dont_have_account">{{lang "login_no_account"}}</div>
|
||||
<div class="formitem dont_have_account">
|
||||
<a href="/accounts/create/">{{lang "login_no_account"}}
|
||||
</div>
|
||||
<div class="formitem forgot_password">
|
||||
<a href="/accounts/password-reset/">{{lang "login_forgot_password"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{{template "header.html" . }}
|
||||
<main id="password_reset_page">
|
||||
<div class="rowblock rowhead">
|
||||
<div class="rowitem"><h1>{{lang "password_reset_head"}}</h1></div>
|
||||
</div>
|
||||
<div class="rowblock the_form">
|
||||
<form action="/accounts/password-reset/submit/" method="post">
|
||||
<div class="formrow login_name_row">
|
||||
<div class="formitem formlabel"><a id="login_name_label">{{lang "password_reset_username"}}</a></div>
|
||||
<div class="formitem"><input name="username" type="text" aria-labelledby="login_name_label" required /></div>
|
||||
</div>
|
||||
<div class="formrow login_button_row form_button_row">
|
||||
<div class="formitem"><button name="login-button" class="formbutton">{{lang "password_reset_button"}}</button></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
{{template "footer.html" . }}
|
|
@ -0,0 +1,30 @@
|
|||
{{template "header.html" . }}
|
||||
<main id="password_reset_page">
|
||||
<div class="rowblock rowhead">
|
||||
<div class="rowitem"><h1>{{lang "password_reset_token_head"}}</h1></div>
|
||||
</div>
|
||||
<div class="rowblock the_form">
|
||||
<form action="/accounts/password-reset/token/submit/" method="post">
|
||||
<input name="uid" value="{{.UID}}" type="hidden" />
|
||||
<input name="token" value="{{.Token}}" type="hidden" />
|
||||
<div class="formrow">
|
||||
<div class="formitem formlabel"><a id="password_label">{{lang "password_reset_token_password"}}</a></div>
|
||||
<div class="formitem"><input name="password" type="password" autocomplete="new-password" placeholder="*****" aria-labelledby="password_label" required /></div>
|
||||
</div>
|
||||
<div class="formrow">
|
||||
<div class="formitem formlabel"><a id="confirm_password_label">{{lang "password_reset_token_confirm_password"}}</a></div>
|
||||
<div class="formitem"><input name="confirm_password" type="password" placeholder="*****" autocomplete="new-password" aria-labelledby="confirm_password_label" required /></div>
|
||||
</div>
|
||||
{{if .MFA}}
|
||||
<div class="formrow mfa_token_row">
|
||||
<div class="formitem formlabel"><a id="mfa_token_label">{{lang "password_reset_mfa_token"}}</a></div>
|
||||
<div class="formitem"><input name="mfa_token" type="text" autocomplete="off" placeholder="*****" aria-labelledby="mfa_token_label" required /></div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="formrow login_button_row form_button_row">
|
||||
<div class="formitem"><button name="token-button" class="formbutton">{{lang "password_reset_token_button"}}</button></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
{{template "footer.html" . }}
|
|
@ -1387,12 +1387,19 @@ textarea {
|
|||
.login_button_row {
|
||||
display: flex;
|
||||
}
|
||||
.dont_have_account {
|
||||
.dont_have_account, .forgot_password {
|
||||
color: var(--primary-link-color);
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
margin-top: 23px;
|
||||
}
|
||||
.dont_have_account {
|
||||
margin-left: auto;
|
||||
}
|
||||
.dont_have_account:after {
|
||||
content: "|";
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* TODO: Highlight the one we're currently on? */
|
||||
.pageset {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
{{template "header.html" . }}
|
||||
<main id="login_page">
|
||||
<div class="rowblock rowhead">
|
||||
<div class="rowitem"><h1>{{lang "login_head"}}</h1></div>
|
||||
</div>
|
||||
<div class="rowblock the_form">
|
||||
<form action="/accounts/login/submit/" method="post">
|
||||
<div class="formrow login_name_row">
|
||||
<div class="formitem formlabel"><a id="login_name_label">{{lang "login_account_name"}}</a></div>
|
||||
<div class="formitem"><input name="username" type="text" placeholder="{{lang "login_account_name"}}" aria-labelledby="login_name_label" required /></div>
|
||||
</div>
|
||||
<div class="formrow login_password_row">
|
||||
<div class="formitem formlabel"><a id="login_password_label">{{lang "login_account_password"}}</a></div>
|
||||
<div class="formitem"><input name="password" type="password" autocomplete="current-password" placeholder="*****" aria-labelledby="login_password_label" required /></div>
|
||||
</div>
|
||||
<div class="formrow login_button_row form_button_row">
|
||||
<div class="formitem"><button name="login-button" class="formbutton">{{lang "login_submit_button"}}</button></div>
|
||||
<div class="fall_opts">
|
||||
<div class="formitem dont_have_account">
|
||||
<a href="/accounts/create/">{{lang "login_no_account"}}
|
||||
</div>
|
||||
<div class="formitem forgot_password">
|
||||
<a href="/accounts/password-reset/">{{lang "login_forgot_password"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
{{template "footer.html" . }}
|
|
@ -614,6 +614,16 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) {
|
|||
.login_mfa_token_row .formlabel {
|
||||
display: none;
|
||||
}
|
||||
.fall_opts {
|
||||
display: flex;
|
||||
}
|
||||
.dont_have_account, .forgot_password {
|
||||
margin-top: 12px;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
.forgot_password {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.pageset {
|
||||
display: flex;
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
{{template "header.html" . }}
|
||||
<main id="login_page">
|
||||
<div class="rowblock rowhead">
|
||||
<div class="rowitem"><h1>{{lang "login_head"}}</h1></div>
|
||||
</div>
|
||||
<div class="rowblock the_form">
|
||||
<form action="/accounts/login/submit/" method="post">
|
||||
<div class="formrow login_name_row">
|
||||
<div class="formitem formlabel"><a id="login_name_label">{{lang "login_account_name"}}</a></div>
|
||||
<div class="formitem"><input name="username" type="text" placeholder="{{lang "login_account_name"}}" aria-labelledby="login_name_label" required /></div>
|
||||
</div>
|
||||
<div class="formrow login_password_row">
|
||||
<div class="formitem formlabel"><a id="login_password_label">{{lang "login_account_password"}}</a></div>
|
||||
<div class="formitem"><input name="password" type="password" autocomplete="current-password" placeholder="*****" aria-labelledby="login_password_label" required /></div>
|
||||
</div>
|
||||
<div class="formrow login_button_row form_button_row">
|
||||
<div class="formitem"><button name="login-button" class="formbutton">{{lang "login_submit_button"}}</button></div>
|
||||
<div class="fall_opts">
|
||||
<div class="formitem dont_have_account">
|
||||
<a href="/accounts/create/">{{lang "login_no_account"}}
|
||||
</div>
|
||||
<div class="formitem forgot_password">
|
||||
<a href="/accounts/password-reset/">{{lang "login_forgot_password"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
{{template "footer.html" . }}
|
|
@ -346,11 +346,32 @@ h1, h2, h3 {
|
|||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.login_button_row {
|
||||
display: flex;
|
||||
}
|
||||
.login_button_row .formitem > * {
|
||||
padding-top: 5px;
|
||||
}
|
||||
.fall_opts {
|
||||
display: flex;
|
||||
}
|
||||
.dont_have_account {
|
||||
color: #505050;
|
||||
margin-left: auto;
|
||||
padding-right: 0px;
|
||||
}
|
||||
.dont_have_account:after {
|
||||
content: "|";
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.forgot_password {
|
||||
padding-left: 0px;
|
||||
}
|
||||
.formitem.dont_have_account, .formitem.forgot_password {
|
||||
color: #909090;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
float: right;
|
||||
padding-top: 11px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
{{template "header.html" . }}
|
||||
<main id="login_page">
|
||||
<div class="rowblock rowhead">
|
||||
<div class="rowitem"><h1>{{lang "login_head"}}</h1></div>
|
||||
</div>
|
||||
<div class="rowblock the_form">
|
||||
<form action="/accounts/login/submit/" method="post">
|
||||
<div class="formrow login_name_row">
|
||||
<div class="formitem formlabel"><a id="login_name_label">{{lang "login_account_name"}}</a></div>
|
||||
<div class="formitem"><input name="username" type="text" placeholder="{{lang "login_account_name"}}" aria-labelledby="login_name_label" required /></div>
|
||||
</div>
|
||||
<div class="formrow login_password_row">
|
||||
<div class="formitem formlabel"><a id="login_password_label">{{lang "login_account_password"}}</a></div>
|
||||
<div class="formitem"><input name="password" type="password" autocomplete="current-password" placeholder="*****" aria-labelledby="login_password_label" required /></div>
|
||||
</div>
|
||||
<div class="formrow login_button_row form_button_row">
|
||||
<div class="formitem"><button name="login-button" class="formbutton">{{lang "login_submit_button"}}</button></div>
|
||||
<div class="fall_opts">
|
||||
<div class="formitem dont_have_account">
|
||||
<a href="/accounts/create/">{{lang "login_no_account"}}
|
||||
</div>
|
||||
<div class="formitem forgot_password">
|
||||
<a href="/accounts/password-reset/">{{lang "login_forgot_password"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
{{template "footer.html" . }}
|
|
@ -420,11 +420,26 @@ input, select {
|
|||
border-color: hsl(0, 0%, 80%);
|
||||
}
|
||||
|
||||
.dont_have_account {
|
||||
color: #505050;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
.fall_opts {
|
||||
float: right;
|
||||
display: flex;
|
||||
}
|
||||
.dont_have_account, .forgot_password {
|
||||
color: #505050;
|
||||
font-size: 14px;
|
||||
margin-top: 6px;
|
||||
border-right: none !important;
|
||||
}
|
||||
.dont_have_account:after {
|
||||
content: "|";
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.dont_have_account {
|
||||
padding-right: 0px;
|
||||
}
|
||||
.forgot_password {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.ip_search_block {
|
Before Width: | Height: | Size: 539 B After Width: | Height: | Size: 539 B |
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 230 KiB |
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "tempra-simple",
|
||||
"Name": "tempra_simple",
|
||||
"FriendlyName": "Tempra Simple",
|
||||
"Version": "0.1.0-dev",
|
||||
"Creator": "Azareal",
|
||||
"FullImage": "tempra-simple.png",
|
||||
"FullImage": "tempra_simple.png",
|
||||
"MobileFriendly": true,
|
||||
"URL": "github.com/Azareal/Gosora",
|
||||
"BgAvatars":true,
|
||||
|
@ -16,7 +16,7 @@
|
|||
],
|
||||
"Resources": [
|
||||
{
|
||||
"Name":"tempra-simple/misc.js",
|
||||
"Name":"tempra_simple/misc.js",
|
||||
"Location":"global"
|
||||
}
|
||||
]
|
Loading…
Reference in New Issue