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
|
// TODO: Implement password resets
|
||||||
/*qgen.Install.CreateTable("password_resets", "", "",
|
qgen.Install.CreateTable("password_resets", "", "",
|
||||||
[]tblColumn{
|
[]tblColumn{
|
||||||
tblColumn{"email", "varchar", 200, false, false, ""},
|
tblColumn{"email", "varchar", 200, false, false, ""},
|
||||||
tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
|
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{"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{"token", "varchar", 200, false, false, ""},
|
||||||
},
|
tblColumn{"createdAt", "createdAt", 0, false, false, ""},
|
||||||
[]tblKey{
|
}, nil,
|
||||||
tblKey{"email", "unique"},
|
)
|
||||||
},
|
|
||||||
)*/
|
|
||||||
|
|
||||||
qgen.Install.CreateTable("forums", mysqlPre, mysqlCol,
|
qgen.Install.CreateTable("forums", mysqlPre, mysqlCol,
|
||||||
[]tblColumn{
|
[]tblColumn{
|
||||||
|
|
|
@ -16,8 +16,9 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Azareal/Gosora/query_gen"
|
|
||||||
"github.com/Azareal/Gosora/common/gauth"
|
"github.com/Azareal/Gosora/common/gauth"
|
||||||
|
"github.com/Azareal/Gosora/query_gen"
|
||||||
|
|
||||||
//"golang.org/x/crypto/argon2"
|
//"golang.org/x/crypto/argon2"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"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 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 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 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 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 ErrNoUserByName = errors.New("We couldn't find an account with that username.")
|
||||||
var DefaultHashAlgo = "bcrypt" // Override this in the configuration file, not here
|
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)
|
LogError(err)
|
||||||
return ErrSecretError
|
return ErrSecretError
|
||||||
}
|
}
|
||||||
if err != ErrNoRows {
|
if err == ErrNoRows {
|
||||||
|
return ErrNoMFAToken
|
||||||
|
}
|
||||||
|
|
||||||
ok, err := VerifyGAuthToken(mfaItem.Secret, mfaToken)
|
ok, err := VerifyGAuthToken(mfaItem.Secret, mfaToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrBadMFAToken
|
return ErrBadMFAToken
|
||||||
|
@ -150,7 +155,6 @@ func (auth *DefaultAuth) ValidateMFAToken(mfaToken string, uid int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return ErrWrongMFAToken
|
return ErrWrongMFAToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ func SendValidationEmail(username string, email string, token string) error {
|
||||||
|
|
||||||
// TODO: Move these to the phrase system
|
// TODO: Move these to the phrase system
|
||||||
subject := "Validate Your Email - " + Site.Name
|
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)
|
return SendEmail(email, subject, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ package common
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -25,6 +27,7 @@ var staticFileMutex sync.RWMutex
|
||||||
type SFile struct {
|
type SFile struct {
|
||||||
Data []byte
|
Data []byte
|
||||||
GzipData []byte
|
GzipData []byte
|
||||||
|
Sha256 []byte
|
||||||
Pos int64
|
Pos int64
|
||||||
Length int64
|
Length int64
|
||||||
GzipLength int64
|
GzipLength int64
|
||||||
|
@ -234,7 +237,12 @@ func (list SFileList) JSTmplInit() error {
|
||||||
return err
|
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)
|
DebugLogf("Added the '%s' static file.", path)
|
||||||
return nil
|
return nil
|
||||||
|
@ -256,6 +264,11 @@ func (list SFileList) Init() error {
|
||||||
var ext = filepath.Ext("/public/" + path)
|
var ext = filepath.Ext("/public/" + path)
|
||||||
mimetype := mime.TypeByExtension(ext)
|
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
|
// Avoid double-compressing images
|
||||||
var gzipData []byte
|
var gzipData []byte
|
||||||
if mimetype != "image/jpeg" && mimetype != "image/png" && mimetype != "image/gif" {
|
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)
|
DebugLogf("Added the '%s' static file.", path)
|
||||||
return nil
|
return nil
|
||||||
|
@ -302,7 +315,12 @@ func (list SFileList) Add(path string, prefix string) error {
|
||||||
return err
|
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)
|
DebugLogf("Added the '%s' static file", path)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -203,6 +203,13 @@ type LevelListPage struct {
|
||||||
Levels []LevelListItem
|
Levels []LevelListItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResetPage struct {
|
||||||
|
*Header
|
||||||
|
UID int
|
||||||
|
Token string
|
||||||
|
MFA bool
|
||||||
|
}
|
||||||
|
|
||||||
type PanelStats struct {
|
type PanelStats struct {
|
||||||
Users int
|
Users int
|
||||||
Groups 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("alert", alertTmpl)
|
||||||
/*//writeTemplate("forum", forumTmpl)
|
/*//writeTemplate("forum", forumTmpl)
|
||||||
writeTemplate("topics_topic", topicListItemTmpl)
|
|
||||||
writeTemplate("topic_posts", topicPostsTmpl)
|
writeTemplate("topic_posts", topicPostsTmpl)
|
||||||
writeTemplate("topic_alt_posts", topicAltPostsTmpl)
|
writeTemplate("topic_alt_posts", topicAltPostsTmpl)
|
||||||
writeTemplate("paginator", paginatorTmpl)
|
|
||||||
//writeTemplate("panel_themes_widgets_widget", panelWidgetsWidgetTmpl)
|
|
||||||
writeTemplateList(c, &wg, dirPrefix)*/
|
writeTemplateList(c, &wg, dirPrefix)*/
|
||||||
return nil
|
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 {
|
func getTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string) string {
|
||||||
DebugLog("in getTemplateList")
|
DebugLog("in getTemplateList")
|
||||||
pout := "\n// nolint\nfunc init() {\n"
|
pout := "\n// nolint\nfunc init() {\n"
|
||||||
|
|
|
@ -3,7 +3,9 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
htmpl "html/template"
|
htmpl "html/template"
|
||||||
"io"
|
"io"
|
||||||
|
@ -157,7 +159,12 @@ func (theme *Theme) AddThemeStaticFiles() error {
|
||||||
return err
|
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 + ".")
|
DebugLog("Added the '/" + theme.Name + path + "' static file for theme " + theme.Name + ".")
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -152,6 +152,8 @@ func (s *DefaultTopicStore) BulkGetMap(ids []int) (list map[int]*Topic, err erro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return list, err
|
return list, err
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
topic := &Topic{}
|
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)
|
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)
|
s.cache.Set(topic)
|
||||||
list[topic.ID] = topic
|
list[topic.ID] = topic
|
||||||
}
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
|
||||||
// Did we miss any topics?
|
// Did we miss any topics?
|
||||||
if idCount > len(list) {
|
if idCount > len(list) {
|
||||||
|
|
|
@ -186,6 +186,8 @@ func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err erro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return list, err
|
return list, err
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
user := &User{Loggedin: true}
|
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)
|
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)
|
mus.cache.Set(user)
|
||||||
list[user.ID] = user
|
list[user.ID] = user
|
||||||
}
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
|
||||||
// Did we miss any users?
|
// Did we miss any users?
|
||||||
if idCount > len(list) {
|
if idCount > len(list) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
||||||
"github.com/Azareal/Gosora/common/phrases"
|
"github.com/Azareal/Gosora/common/phrases"
|
||||||
|
min "github.com/Azareal/Gosora/common/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
type wolUsers struct {
|
type wolUsers struct {
|
||||||
|
@ -53,6 +54,10 @@ func wolTick(widget *Widget) error {
|
||||||
}
|
}
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
buf.ReadFrom(w.Result().Body)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,6 +154,10 @@ var RouteMap = map[string]interface{}{
|
||||||
"routes.AccountLoginMFAVerify": routes.AccountLoginMFAVerify,
|
"routes.AccountLoginMFAVerify": routes.AccountLoginMFAVerify,
|
||||||
"routes.AccountLoginMFAVerifySubmit": routes.AccountLoginMFAVerifySubmit,
|
"routes.AccountLoginMFAVerifySubmit": routes.AccountLoginMFAVerifySubmit,
|
||||||
"routes.AccountRegisterSubmit": routes.AccountRegisterSubmit,
|
"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.DynamicRoute": routes.DynamicRoute,
|
||||||
"routes.UploadedFile": routes.UploadedFile,
|
"routes.UploadedFile": routes.UploadedFile,
|
||||||
"routes.StaticFile": routes.StaticFile,
|
"routes.StaticFile": routes.StaticFile,
|
||||||
|
@ -295,12 +299,16 @@ var routeMapEnum = map[string]int{
|
||||||
"routes.AccountLoginMFAVerify": 128,
|
"routes.AccountLoginMFAVerify": 128,
|
||||||
"routes.AccountLoginMFAVerifySubmit": 129,
|
"routes.AccountLoginMFAVerifySubmit": 129,
|
||||||
"routes.AccountRegisterSubmit": 130,
|
"routes.AccountRegisterSubmit": 130,
|
||||||
"routes.DynamicRoute": 131,
|
"routes.AccountPasswordReset": 131,
|
||||||
"routes.UploadedFile": 132,
|
"routes.AccountPasswordResetSubmit": 132,
|
||||||
"routes.StaticFile": 133,
|
"routes.AccountPasswordResetToken": 133,
|
||||||
"routes.RobotsTxt": 134,
|
"routes.AccountPasswordResetTokenSubmit": 134,
|
||||||
"routes.SitemapXml": 135,
|
"routes.DynamicRoute": 135,
|
||||||
"routes.BadRoute": 136,
|
"routes.UploadedFile": 136,
|
||||||
|
"routes.StaticFile": 137,
|
||||||
|
"routes.RobotsTxt": 138,
|
||||||
|
"routes.SitemapXml": 139,
|
||||||
|
"routes.BadRoute": 140,
|
||||||
}
|
}
|
||||||
var reverseRouteMapEnum = map[int]string{
|
var reverseRouteMapEnum = map[int]string{
|
||||||
0: "routes.Overview",
|
0: "routes.Overview",
|
||||||
|
@ -434,12 +442,16 @@ var reverseRouteMapEnum = map[int]string{
|
||||||
128: "routes.AccountLoginMFAVerify",
|
128: "routes.AccountLoginMFAVerify",
|
||||||
129: "routes.AccountLoginMFAVerifySubmit",
|
129: "routes.AccountLoginMFAVerifySubmit",
|
||||||
130: "routes.AccountRegisterSubmit",
|
130: "routes.AccountRegisterSubmit",
|
||||||
131: "routes.DynamicRoute",
|
131: "routes.AccountPasswordReset",
|
||||||
132: "routes.UploadedFile",
|
132: "routes.AccountPasswordResetSubmit",
|
||||||
133: "routes.StaticFile",
|
133: "routes.AccountPasswordResetToken",
|
||||||
134: "routes.RobotsTxt",
|
134: "routes.AccountPasswordResetTokenSubmit",
|
||||||
135: "routes.SitemapXml",
|
135: "routes.DynamicRoute",
|
||||||
136: "routes.BadRoute",
|
136: "routes.UploadedFile",
|
||||||
|
137: "routes.StaticFile",
|
||||||
|
138: "routes.RobotsTxt",
|
||||||
|
139: "routes.SitemapXml",
|
||||||
|
140: "routes.BadRoute",
|
||||||
}
|
}
|
||||||
var osMapEnum = map[string]int{
|
var osMapEnum = map[string]int{
|
||||||
"unknown": 0,
|
"unknown": 0,
|
||||||
|
@ -738,7 +750,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
counters.GlobalViewCounter.Bump()
|
counters.GlobalViewCounter.Bump()
|
||||||
|
|
||||||
if prefix == "/static" {
|
if prefix == "/static" {
|
||||||
counters.RouteViewCounter.Bump(133)
|
counters.RouteViewCounter.Bump(137)
|
||||||
req.URL.Path += extraData
|
req.URL.Path += extraData
|
||||||
routes.StaticFile(w, req)
|
routes.StaticFile(w, req)
|
||||||
return
|
return
|
||||||
|
@ -2085,6 +2097,36 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
||||||
|
|
||||||
counters.RouteViewCounter.Bump(130)
|
counters.RouteViewCounter.Bump(130)
|
||||||
err = routes.AccountRegisterSubmit(w,req,user)
|
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
|
/*case "/sitemaps": // TODO: Count these views
|
||||||
req.URL.Path += extraData
|
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-Type")
|
||||||
w.Header().Del("Content-Encoding")
|
w.Header().Del("Content-Encoding")
|
||||||
}
|
}
|
||||||
counters.RouteViewCounter.Bump(132)
|
counters.RouteViewCounter.Bump(136)
|
||||||
req.URL.Path += extraData
|
req.URL.Path += extraData
|
||||||
// TODO: Find a way to propagate errors up from this?
|
// TODO: Find a way to propagate errors up from this?
|
||||||
r.UploadHandler(w,req) // TODO: Count these views
|
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
|
// TODO: Add support for favicons and robots.txt files
|
||||||
switch(extraData) {
|
switch(extraData) {
|
||||||
case "robots.txt":
|
case "robots.txt":
|
||||||
counters.RouteViewCounter.Bump(134)
|
counters.RouteViewCounter.Bump(138)
|
||||||
return routes.RobotsTxt(w,req)
|
return routes.RobotsTxt(w,req)
|
||||||
case "favicon.ico":
|
case "favicon.ico":
|
||||||
req.URL.Path = "/static"+req.URL.Path+extraData
|
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)
|
routes.StaticFile(w,req)
|
||||||
return nil
|
return nil
|
||||||
/*case "sitemap.xml":
|
/*case "sitemap.xml":
|
||||||
counters.RouteViewCounter.Bump(135)
|
counters.RouteViewCounter.Bump(139)
|
||||||
return routes.SitemapXml(w,req)*/
|
return routes.SitemapXml(w,req)*/
|
||||||
}
|
}
|
||||||
return common.NotFound(w,req,nil)
|
return common.NotFound(w,req,nil)
|
||||||
|
@ -2128,7 +2170,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
||||||
r.RUnlock()
|
r.RUnlock()
|
||||||
|
|
||||||
if ok {
|
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
|
req.URL.Path += extraData
|
||||||
return handle(w,req,user)
|
return handle(w,req,user)
|
||||||
}
|
}
|
||||||
|
@ -2139,7 +2181,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
|
||||||
} else {
|
} else {
|
||||||
r.DumpRequest(req,"Bad Route")
|
r.DumpRequest(req,"Bad Route")
|
||||||
}
|
}
|
||||||
counters.RouteViewCounter.Bump(136)
|
counters.RouteViewCounter.Bump(140)
|
||||||
return common.NotFound(w,req,nil)
|
return common.NotFound(w,req,nil)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -100,6 +100,8 @@
|
||||||
"register_username_too_long_prefix":"The username is too long, max: ",
|
"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.",
|
"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_actor":"Unable to find the actor",
|
||||||
"alerts_no_target_user":"Unable to find the target user",
|
"alerts_no_target_user":"Unable to find the target user",
|
||||||
"alerts_no_linked_topic":"Unable to find the linked topic",
|
"alerts_no_linked_topic":"Unable to find the linked topic",
|
||||||
|
@ -128,6 +130,8 @@
|
||||||
"login":"Login",
|
"login":"Login",
|
||||||
"login_mfa_verify":"2FA Verify",
|
"login_mfa_verify":"2FA Verify",
|
||||||
"register":"Registration",
|
"register":"Registration",
|
||||||
|
"password_reset":"Password Reset",
|
||||||
|
"password_reset_token":"Password Reset",
|
||||||
"ip_search":"IP Search",
|
"ip_search":"IP Search",
|
||||||
"profile": "%s's Profile",
|
"profile": "%s's Profile",
|
||||||
"account":"My Account",
|
"account":"My Account",
|
||||||
|
@ -305,6 +309,8 @@
|
||||||
"account_mail_disabled":"The mail system is currently disabled.",
|
"account_mail_disabled":"The mail system is currently disabled.",
|
||||||
"account_mail_verify_success":"Your email was successfully verified.",
|
"account_mail_verify_success":"Your email was successfully verified.",
|
||||||
"account_mfa_setup_success":"Two-factor authentication was successfully setup for your account.",
|
"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_created":"The forum was successfully created.",
|
||||||
"panel_forum_deleted":"The forum was successfully deleted.",
|
"panel_forum_deleted":"The forum was successfully deleted.",
|
||||||
|
@ -446,6 +452,7 @@
|
||||||
"login_account_password":"Password",
|
"login_account_password":"Password",
|
||||||
"login_submit_button":"Login",
|
"login_submit_button":"Login",
|
||||||
"login_no_account":"Don't have an account?",
|
"login_no_account":"Don't have an account?",
|
||||||
|
"login_forgot_password":"Forgot your password?",
|
||||||
|
|
||||||
"login_mfa_verify_head":"2FA Verify",
|
"login_mfa_verify_head":"2FA Verify",
|
||||||
"login_mfa_verify_explanation":"Please input the code from the authenticator app below.",
|
"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_account_anti_spam":"Are you a spambot?",
|
||||||
"register_submit_button":"Create Account",
|
"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_head":"My Account",
|
||||||
"account_menu_password":"Password",
|
"account_menu_password":"Password",
|
||||||
"account_menu_email":"Email",
|
"account_menu_email":"Email",
|
||||||
|
|
4
main.go
4
main.go
|
@ -165,6 +165,10 @@ func afterDBInit() (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
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
|
// TODO: Let the admin choose other thumbnailers, maybe ones defined in plugins
|
||||||
common.Thumbnailer = common.NewCaireThumbnailer()
|
common.Thumbnailer = common.NewCaireThumbnailer()
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ func init() {
|
||||||
addPatch(13, patch13)
|
addPatch(13, patch13)
|
||||||
addPatch(14, patch14)
|
addPatch(14, patch14)
|
||||||
addPatch(15, patch15)
|
addPatch(15, patch15)
|
||||||
|
addPatch(16, patch16)
|
||||||
}
|
}
|
||||||
|
|
||||||
func patch0(scanner *bufio.Scanner) (err error) {
|
func patch0(scanner *bufio.Scanner) (err error) {
|
||||||
|
@ -537,3 +538,15 @@ func patch14(scanner *bufio.Scanner) error {
|
||||||
func patch15(scanner *bufio.Scanner) error {
|
func patch15(scanner *bufio.Scanner) error {
|
||||||
return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'google_site_verify','','html-attribute'"))
|
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() {
|
function bindToAlerts() {
|
||||||
|
$(".alertItem.withAvatar a").unbind("click");
|
||||||
$(".alertItem.withAvatar a").click(function(event) {
|
$(".alertItem.withAvatar a").click(function(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
$.ajax({ url: "/api/?action=set&module=dismiss-alert", type: "POST", dataType: "json", error: ajaxError, data: { asid: $(this).attr("data-asid") } });
|
$.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.LevelList", "/user/levels/"),
|
||||||
//MemberView("routes.LevelRankings", "/user/rankings/"),
|
//MemberView("routes.LevelRankings", "/user/rankings/"),
|
||||||
|
//MemberView("routes.Alerts", "/user/alerts/"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,8 +136,11 @@ func accountRoutes() *RouteGroup {
|
||||||
View("routes.AccountLoginMFAVerify", "/accounts/mfa_verify/"),
|
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.AccountLoginMFAVerifySubmit", "/accounts/mfa_verify/submit/"), // We have logic in here which filters out regular guests
|
||||||
AnonAction("routes.AccountRegisterSubmit", "/accounts/create/submit/"),
|
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"
|
"crypto/subtle"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
@ -424,7 +425,7 @@ func AccountEditPasswordSubmit(w http.ResponseWriter, r *http.Request, user comm
|
||||||
if newPassword != confirmPassword {
|
if newPassword != confirmPassword {
|
||||||
return common.LocalError("The two passwords don't match.", w, r, user)
|
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
|
// Log the user out as a safety precaution
|
||||||
common.Auth.ForceLogout(user.ID)
|
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)
|
return common.LocalError("You are not logged in", w, r, user)
|
||||||
}
|
}
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
if email.Token == token {
|
if subtle.ConstantTimeCompare([]byte(email.Token), []byte(token)) == 1 {
|
||||||
targetEmail = email
|
targetEmail = email
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -761,3 +762,154 @@ func LevelList(w http.ResponseWriter, r *http.Request, user common.User, header
|
||||||
pi := common.LevelListPage{header, levels[1:]}
|
pi := common.LevelListPage{header, levels[1:]}
|
||||||
return renderTemplate("level_list", w, r, header, pi)
|
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>
|
||||||
<div class="formrow login_button_row form_button_row">
|
<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"><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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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 {
|
.login_button_row {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.dont_have_account {
|
.dont_have_account, .forgot_password {
|
||||||
color: var(--primary-link-color);
|
color: var(--primary-link-color);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-left: auto;
|
|
||||||
margin-top: 23px;
|
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? */
|
/* TODO: Highlight the one we're currently on? */
|
||||||
.pageset {
|
.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 {
|
.login_mfa_token_row .formlabel {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.fall_opts {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.dont_have_account, .forgot_password {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: -8px;
|
||||||
|
}
|
||||||
|
.forgot_password {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.pageset {
|
.pageset {
|
||||||
display: flex;
|
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;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login_button_row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.login_button_row .formitem > * {
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
.fall_opts {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
.dont_have_account {
|
.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-size: 12px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
float: right;
|
padding-top: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
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%);
|
border-color: hsl(0, 0%, 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dont_have_account {
|
.fall_opts {
|
||||||
color: #505050;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: normal;
|
|
||||||
float: right;
|
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 {
|
.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",
|
"FriendlyName": "Tempra Simple",
|
||||||
"Version": "0.1.0-dev",
|
"Version": "0.1.0-dev",
|
||||||
"Creator": "Azareal",
|
"Creator": "Azareal",
|
||||||
"FullImage": "tempra-simple.png",
|
"FullImage": "tempra_simple.png",
|
||||||
"MobileFriendly": true,
|
"MobileFriendly": true,
|
||||||
"URL": "github.com/Azareal/Gosora",
|
"URL": "github.com/Azareal/Gosora",
|
||||||
"BgAvatars":true,
|
"BgAvatars":true,
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
],
|
],
|
||||||
"Resources": [
|
"Resources": [
|
||||||
{
|
{
|
||||||
"Name":"tempra-simple/misc.js",
|
"Name":"tempra_simple/misc.js",
|
||||||
"Location":"global"
|
"Location":"global"
|
||||||
}
|
}
|
||||||
]
|
]
|
Loading…
Reference in New Issue