From 543ad8a0182e3773d974f2df6106a6569a1fc6a4 Mon Sep 17 00:00:00 2001 From: Azareal Date: Mon, 17 Dec 2018 14:58:55 +1000 Subject: [PATCH] Added the login log. Added a better paginator for view topic. The last post on the topic list and forum pages now link to the last page of a topic for quicker navigation. The Account Manager now utilises dyntmpl in more areas. More tooltips. Tweaked the colour for the validated emails in the Email Manager so it looks nicer on dark themes. Moved some inline styles from the Email Manager into the stylesheets and removed obsolete ones. Added the mixed BenchmarkTopicGuestAdminRouteParallelWithRouter benchmark. The bad route benchmark should no longer abort when erroring, something which doesn't make sense as it's always expected to error. Reduce a bit more boilerplate with renderTemplate() Added the *CTemplateSet.addText method and used it to optimise the generated templates a tiny bit more. The forums route now has guest and member variants generated for it. Turned the experimental template optimisation back on for more data. Added the routes.AccountLogins route. Added the account_logins phrase. Added the account_menu_logins phrase. Added the account_logins_head phrase. Added the account_logins_success phrase. Added the account_logins_failure phrase. You need to run the patcher / updater for this commit. --- cmd/query_gen/tables.go | 31 ++- common/misc_logs.go | 88 +++++++++ common/pages.go | 21 +- common/template_init.go | 36 ++-- common/templates/templates.go | 41 ++-- common/theme.go | 8 +- common/topic.go | 1 + common/topic_list.go | 3 + gen_router.go | 243 +++++++++++++----------- gen_tables.go | 29 +-- general_test.go | 106 +++++++++-- langs/english.json | 6 + main.go | 4 + patcher/patches.go | 181 ++++++++++-------- router_gen/routes.go | 2 + routes/account.go | 59 ++++-- routes/forum.go | 3 + routes/topic.go | 3 +- schema/mssql/query_login_logs.sql | 8 + schema/mssql/query_updates.sql | 3 + schema/mysql/query_login_logs.sql | 8 + schema/mysql/query_updates.sql | 3 + schema/pgsql/query_login_logs.sql | 8 + schema/pgsql/query_updates.sql | 3 + schema/schema.json | 2 +- templates/account.html | 2 +- templates/account_logins.html | 20 ++ templates/account_menu.html | 1 + templates/account_own_edit_email.html | 35 ++-- templates/forum.html | 2 +- templates/panel_adminlogs.html | 2 +- templates/panel_modlogs.html | 2 +- templates/panel_reglogs.html | 2 +- templates/topic_alt.html | 3 + templates/topics_topic.html | 2 +- themes/cosora/public/account.css | 7 + themes/cosora/public/main.css | 4 + themes/nox/public/acc_panel_common.css | 4 + themes/nox/public/account.css | 10 + themes/nox/public/main.css | 3 + themes/nox/public/panel.css | 2 +- themes/shadow/public/account.css | 7 + themes/tempra-simple/public/account.css | 7 + 43 files changed, 687 insertions(+), 328 deletions(-) create mode 100644 schema/mssql/query_login_logs.sql create mode 100644 schema/mssql/query_updates.sql create mode 100644 schema/mysql/query_login_logs.sql create mode 100644 schema/mysql/query_updates.sql create mode 100644 schema/pgsql/query_login_logs.sql create mode 100644 schema/pgsql/query_updates.sql create mode 100644 templates/account_logins.html diff --git a/cmd/query_gen/tables.go b/cmd/query_gen/tables.go index f01f46c8..9d6b5f3c 100644 --- a/cmd/query_gen/tables.go +++ b/cmd/query_gen/tables.go @@ -493,21 +493,18 @@ func createTables(adapter qgen.Adapter) error { }, ) - // TODO: Implement this - /* - qgen.Install.CreateTable("login_logs", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"lid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, - qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? - qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""}, - }, - []qgen.DBTableKey{ - qgen.DBTableKey{"lid", "primary"}, - }, - ) - */ + qgen.Install.CreateTable("login_logs", "", "", + []qgen.DBTableColumn{ + qgen.DBTableColumn{"lid", "int", 0, false, true, ""}, + qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, + qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? + qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, + qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""}, + }, + []qgen.DBTableKey{ + qgen.DBTableKey{"lid", "primary"}, + }, + ) qgen.Install.CreateTable("moderation_logs", "", "", []qgen.DBTableColumn{ @@ -613,12 +610,12 @@ func createTables(adapter qgen.Adapter) error { []qgen.DBTableKey{}, ) - /*qgen.Install.CreateTable("updates", "", "", + qgen.Install.CreateTable("updates", "", "", []qgen.DBTableColumn{ qgen.DBTableColumn{"dbVersion", "int", 0, false, false, "0"}, }, []qgen.DBTableKey{}, - )*/ + ) return nil } diff --git a/common/misc_logs.go b/common/misc_logs.go index 1d8e160f..a76a5ff2 100644 --- a/common/misc_logs.go +++ b/common/misc_logs.go @@ -8,6 +8,7 @@ import ( ) var RegLogs RegLogStore +var LoginLogs LoginLogStore type RegLogItem struct { ID int @@ -97,3 +98,90 @@ func (store *SQLRegLogStore) GetOffset(offset int, perPage int) (logs []RegLogIt } return logs, rows.Err() } + +type LoginLogItem struct { + ID int + UID int + Success bool + IPAddress string + DoneAt string +} + +type LoginLogStmts struct { + update *sql.Stmt + create *sql.Stmt +} + +var loginLogStmts LoginLogStmts + +func init() { + DbInits.Add(func(acc *qgen.Accumulator) error { + loginLogStmts = LoginLogStmts{ + update: acc.Update("login_logs").Set("uid = ?, success = ?").Where("lid = ?").Prepare(), + create: acc.Insert("login_logs").Columns("uid, success, ipaddress, doneAt").Fields("?,?,?,UTC_TIMESTAMP()").Prepare(), + } + return acc.FirstError() + }) +} + +// TODO: Reload this item in the store, probably doesn't matter right now, but it might when we start caching this stuff in memory +// ! Retroactive updates of date are not permitted for integrity reasons +func (log *LoginLogItem) Commit() error { + _, err := loginLogStmts.update.Exec(log.UID, log.Success, log.ID) + return err +} + +func (log *LoginLogItem) Create() (id int, err error) { + res, err := loginLogStmts.create.Exec(log.UID, log.Success, log.IPAddress) + if err != nil { + return 0, err + } + id64, err := res.LastInsertId() + log.ID = int(id64) + return log.ID, err +} + +type LoginLogStore interface { + GlobalCount() (logCount int) + GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error) +} + +type SQLLoginLogStore struct { + count *sql.Stmt + getOffsetByUser *sql.Stmt +} + +func NewLoginLogStore(acc *qgen.Accumulator) (*SQLLoginLogStore, error) { + return &SQLLoginLogStore{ + count: acc.Count("login_logs").Prepare(), + getOffsetByUser: acc.Select("login_logs").Columns("lid, success, ipaddress, doneAt").Where("uid = ?").Orderby("doneAt DESC").Limit("?,?").Prepare(), + }, acc.FirstError() +} + +func (store *SQLLoginLogStore) GlobalCount() (logCount int) { + err := store.count.QueryRow().Scan(&logCount) + if err != nil { + LogError(err) + } + return logCount +} + +func (store *SQLLoginLogStore) GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error) { + rows, err := store.getOffsetByUser.Query(uid, offset, perPage) + if err != nil { + return logs, err + } + defer rows.Close() + + for rows.Next() { + var log = LoginLogItem{UID: uid} + var doneAt time.Time + err := rows.Scan(&log.ID, &log.Success, &log.IPAddress, &doneAt) + if err != nil { + return logs, err + } + log.DoneAt = doneAt.Format("2006-01-02 15:04:05") + logs = append(logs, log) + } + return logs, rows.Err() +} diff --git a/common/pages.go b/common/pages.go index 63e3a83c..5ba0ae10 100644 --- a/common/pages.go +++ b/common/pages.go @@ -104,8 +104,7 @@ type TopicPage struct { Topic TopicUser Forum *Forum Poll Poll - Page int - LastPage int + Paginator } type TopicListSort struct { @@ -154,16 +153,26 @@ type IPSearchPage struct { IP string } +type Account struct { + *Header + HTMLID string + TmplName string + Inner nobreak +} + type EmailListPage struct { *Header - ItemList []Email - Something interface{} + ItemList []Email +} + +type AccountLoginsPage struct { + *Header + ItemList []LoginLogItem + Paginator } type AccountDashPage struct { *Header - HTMLID string - TmplName string MFASetup bool CurrentScore int NextScore int diff --git a/common/template_init.go b/common/template_init.go index a275c53d..c40c68aa 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -4,7 +4,6 @@ import ( "html/template" "io" "log" - "math" "path/filepath" "strconv" "strings" @@ -85,6 +84,8 @@ var Template_forums_handle = func(pi ForumsPage, w io.Writer) error { } return Templates.ExecuteTemplate(w, mapping+".html", pi) } +var Template_forums_guest_handle = Template_forums_handle +var Template_forums_member_handle = Template_forums_handle // nolint var Template_profile_handle = func(pi ProfilePage, w io.Writer) error { @@ -143,7 +144,7 @@ var Template_ip_search_handle = func(pi IPSearchPage, w io.Writer) error { } // nolint -var Template_account_handle = func(pi AccountDashPage, w io.Writer) error { +var Template_account_handle = func(pi Account, w io.Writer) error { mapping, ok := Themes[DefaultThemeBox.Load().(string)].TemplatesMap["account"] if !ok { mapping = "account" @@ -195,6 +196,8 @@ type TmplLoggedin struct { Member string } +type nobreak interface{} + // ? - Add template hooks? func CompileTemplates() error { var config tmpl.CTemplateConfig @@ -239,7 +242,7 @@ func CompileTemplates() error { } header.Title = "Topic Name" - tpage := TopicPage{header, replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, poll, 1, 1} + 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) topicTmpl, err := compile("topic", "common.TopicPage", tpage) if err != nil { @@ -276,13 +279,13 @@ func CompileTemplates() error { varList = make(map[string]tmpl.VarItem) header.Title = "Forum List" forumsPage := ForumsPage{header, forumList} - forumsTmpl, err := compile("forums", "common.ForumsPage", forumsPage) + forumsTmpl, err := compileByLoggedin("forums", "common.ForumsPage", forumsPage) if err != nil { return err } var topicsList []*TopicsRow - topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 1, 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) + topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 1, 0, 1, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) header2.Title = "Topic List" topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}} /*topicListTmpl, err := compile("topics", "common.TopicListPage", topicListPage) @@ -332,14 +335,9 @@ func CompileTemplates() error { return err } - mfaSetup := false - prevScore := GetLevelScore(header.CurrentUser.Level) - currentScore := header.CurrentUser.Score - prevScore - nextScore := GetLevelScore(header.CurrentUser.Level+1) - prevScore - perc := int(math.Ceil((float64(nextScore) / float64(currentScore)) * 100)) - - accountPage := AccountDashPage{header, "dashboard", "account_own_edit", mfaSetup, currentScore, nextScore, user.Level + 1, perc * 2} - accountTmpl, err := compile("account", "common.AccountDashPage", accountPage) + var inter nobreak + accountPage := Account{header, "dashboard", "account_own_edit", inter} + accountTmpl, err := compile("account", "common.Account", accountPage) if err != nil { return err } @@ -441,7 +439,7 @@ func CompileJSTemplates() error { // 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, "Date", user3.ID, 1, "", "127.0.0.1", 1, 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"} + var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 1, 0, 1, 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 @@ -460,7 +458,7 @@ func CompileJSTemplates() error { varList = make(map[string]tmpl.VarItem) header.Title = "Topic Name" - tpage := TopicPage{header, replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, poll, 1, 1} + 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 { @@ -509,16 +507,16 @@ func writeTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string) } getterstr += "}\nreturn nil\n}\n" out += "\n// nolint\nfunc init() {\n" - //var bodyMap = make(map[string]string) //map[body]fragmentPrefix - var tmplMap = make(map[string]map[string]string) // map[tmpl]map[body]fragmentPrefix + var bodyMap = make(map[string]string) //map[body]fragmentPrefix + //var tmplMap = make(map[string]map[string]string) // map[tmpl]map[body]fragmentPrefix var tmpCount = 0 for _, frag := range c.FragOut { front := frag.TmplName + "_frags[" + strconv.Itoa(frag.Index) + "]" - bodyMap, tok := tmplMap[frag.TmplName] + /*bodyMap, tok := tmplMap[frag.TmplName] if !tok { tmplMap[frag.TmplName] = make(map[string]string) bodyMap = tmplMap[frag.TmplName] - } + }*/ fp, ok := bodyMap[frag.Body] if !ok { bodyMap[frag.Body] = front diff --git a/common/templates/templates.go b/common/templates/templates.go index a23504f9..2289caf0 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -520,21 +520,25 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) { case *parse.TemplateNode: c.compileSubTemplate(con, node) case *parse.TextNode: - tmpText := bytes.TrimSpace(node.Text) - if len(tmpText) == 0 { - return - } - nodeText := string(node.Text) - fragIndex := c.fragmentCursor[con.TemplateName] - _, ok := c.FragOnce[con.TemplateName] - c.fragBuf = append(c.fragBuf, Fragment{nodeText, con.TemplateName, fragIndex, ok}) - con.PushText(strconv.Itoa(fragIndex), fragIndex, len(c.fragBuf)-1) - c.fragmentCursor[con.TemplateName] = fragIndex + 1 + c.addText(con, node.Text) default: c.unknownNode(node) } } +func (c *CTemplateSet) addText(con CContext, text []byte) { + tmpText := bytes.TrimSpace(text) + if len(tmpText) == 0 { + return + } + nodeText := string(text) + fragIndex := c.fragmentCursor[con.TemplateName] + _, ok := c.FragOnce[con.TemplateName] + c.fragBuf = append(c.fragBuf, Fragment{nodeText, con.TemplateName, fragIndex, ok}) + con.PushText(strconv.Itoa(fragIndex), fragIndex, len(c.fragBuf)-1) + c.fragmentCursor[con.TemplateName] = fragIndex + 1 +} + func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) { c.dumpCall("compileRangeNode", con, node) defer c.retCall("compileRangeNode") @@ -1050,11 +1054,14 @@ func (c *CTemplateSet) compileIfVarSub(con CContext, varname string) (out string } var stepInterface = func() { - if cur.Kind() == reflect.Interface { + var nobreak = (cur.Type().Name() == "nobreak") + c.detailf("cur.Type().Name(): %+v\n", cur.Type().Name()) + if cur.Kind() == reflect.Interface && !nobreak { cur = cur.Elem() out += ".(" + cur.Type().Name() + ")" } } + bits := strings.Split(varname, ".") if varname[0] == '$' { var res VarItemReflect @@ -1270,30 +1277,30 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V c.detail("optimising away member branch") if inSlice(userExprs, varname) { c.detail("positive conditional:", varname) - con.Push("varsub", "[]byte(\"false\")") + c.addText(con, []byte("false")) return } else if inSlice(negUserExprs, varname) { c.detail("negative conditional:", varname) - con.Push("varsub", "[]byte(\"true\")") + c.addText(con, []byte("true")) return } } else if c.memberOnly { c.detail("optimising away guest branch") if (con.RootHolder + ".CurrentUser.Loggedin") == varname { c.detail("positive conditional:", varname) - con.Push("varsub", "[]byte(\"true\")") + c.addText(con, []byte("true")) return } else if ("!" + con.RootHolder + ".CurrentUser.Loggedin") == varname { c.detail("negative conditional:", varname) - con.Push("varsub", "[]byte(\"false\")") + c.addText(con, []byte("false")) return } } con.Push("startif", "if "+varname+" {\n") - con.Push("varsub", "[]byte(\"true\")") + c.addText(con, []byte("true")) con.Push("endif", "} ") con.Push("startelse", "else {\n") - con.Push("varsub", "[]byte(\"false\")") + c.addText(con, []byte("false")) con.Push("endelse", "}\n") return case reflect.String: diff --git a/common/theme.go b/common/theme.go index 272635f6..122fc98b 100644 --- a/common/theme.go +++ b/common/theme.go @@ -390,9 +390,9 @@ func (theme *Theme) RunTmpl(template string, pi interface{}, w io.Writer) error case *func(IPSearchPage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(IPSearchPage), w) - case *func(AccountDashPage, io.Writer) error: + case *func(Account, io.Writer) error: var tmpl = *tmplO - return tmpl(pi.(AccountDashPage), w) + return tmpl(pi.(Account), w) case *func(ErrorPage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(ErrorPage), w) @@ -415,8 +415,8 @@ func (theme *Theme) RunTmpl(template string, pi interface{}, w io.Writer) error return tmplO(pi.(CreateTopicPage), w) case func(IPSearchPage, io.Writer) error: return tmplO(pi.(IPSearchPage), w) - case func(AccountDashPage, io.Writer) error: - return tmplO(pi.(AccountDashPage), w) + case func(Account, io.Writer) error: + return tmplO(pi.(Account), w) case func(ErrorPage, io.Writer) error: return tmplO(pi.(ErrorPage), w) case func(Page, io.Writer) error: diff --git a/common/topic.go b/common/topic.go index 1bebc2f0..5363f279 100644 --- a/common/topic.go +++ b/common/topic.go @@ -102,6 +102,7 @@ type TopicsRow struct { ViewCount int64 PostCount int LikeCount int + LastPage int ClassName string Data string // Used for report metadata diff --git a/common/topic_list.go b/common/topic_list.go index 7945879b..3cd52a2e 100644 --- a/common/topic_list.go +++ b/common/topic_list.go @@ -243,6 +243,9 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter //topicItem.RelativeCreatedAt = RelativeTime(topicItem.CreatedAt) topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt) + // TODO: Create a specialised function with a bit less overhead for getting the last page for a post count + _, _, lastPage := PageOffset(topicItem.PostCount, 1, Config.ItemsPerPage) + topicItem.LastPage = lastPage // TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/ GetHookTable().Vhook("topics_topic_row_assign", &topicItem, &forum) diff --git a/gen_router.go b/gen_router.go index 34cd7bcb..bdc7e98c 100644 --- a/gen_router.go +++ b/gen_router.go @@ -112,6 +112,7 @@ var RouteMap = map[string]interface{}{ "routes.AccountEditMFADisableSubmit": routes.AccountEditMFADisableSubmit, "routes.AccountEditEmail": routes.AccountEditEmail, "routes.AccountEditEmailTokenSubmit": routes.AccountEditEmailTokenSubmit, + "routes.AccountLogins": routes.AccountLogins, "routes.LevelList": routes.LevelList, "routes.ViewProfile": routes.ViewProfile, "routes.BanUserSubmit": routes.BanUserSubmit, @@ -244,44 +245,45 @@ var routeMapEnum = map[string]int{ "routes.AccountEditMFADisableSubmit": 87, "routes.AccountEditEmail": 88, "routes.AccountEditEmailTokenSubmit": 89, - "routes.LevelList": 90, - "routes.ViewProfile": 91, - "routes.BanUserSubmit": 92, - "routes.UnbanUser": 93, - "routes.ActivateUser": 94, - "routes.IPSearch": 95, - "routes.CreateTopicSubmit": 96, - "routes.EditTopicSubmit": 97, - "routes.DeleteTopicSubmit": 98, - "routes.StickTopicSubmit": 99, - "routes.UnstickTopicSubmit": 100, - "routes.LockTopicSubmit": 101, - "routes.UnlockTopicSubmit": 102, - "routes.MoveTopicSubmit": 103, - "routes.LikeTopicSubmit": 104, - "routes.ViewTopic": 105, - "routes.CreateReplySubmit": 106, - "routes.ReplyEditSubmit": 107, - "routes.ReplyDeleteSubmit": 108, - "routes.ReplyLikeSubmit": 109, - "routes.ProfileReplyCreateSubmit": 110, - "routes.ProfileReplyEditSubmit": 111, - "routes.ProfileReplyDeleteSubmit": 112, - "routes.PollVote": 113, - "routes.PollResults": 114, - "routes.AccountLogin": 115, - "routes.AccountRegister": 116, - "routes.AccountLogout": 117, - "routes.AccountLoginSubmit": 118, - "routes.AccountLoginMFAVerify": 119, - "routes.AccountLoginMFAVerifySubmit": 120, - "routes.AccountRegisterSubmit": 121, - "routes.DynamicRoute": 122, - "routes.UploadedFile": 123, - "routes.StaticFile": 124, - "routes.RobotsTxt": 125, - "routes.SitemapXml": 126, - "routes.BadRoute": 127, + "routes.AccountLogins": 90, + "routes.LevelList": 91, + "routes.ViewProfile": 92, + "routes.BanUserSubmit": 93, + "routes.UnbanUser": 94, + "routes.ActivateUser": 95, + "routes.IPSearch": 96, + "routes.CreateTopicSubmit": 97, + "routes.EditTopicSubmit": 98, + "routes.DeleteTopicSubmit": 99, + "routes.StickTopicSubmit": 100, + "routes.UnstickTopicSubmit": 101, + "routes.LockTopicSubmit": 102, + "routes.UnlockTopicSubmit": 103, + "routes.MoveTopicSubmit": 104, + "routes.LikeTopicSubmit": 105, + "routes.ViewTopic": 106, + "routes.CreateReplySubmit": 107, + "routes.ReplyEditSubmit": 108, + "routes.ReplyDeleteSubmit": 109, + "routes.ReplyLikeSubmit": 110, + "routes.ProfileReplyCreateSubmit": 111, + "routes.ProfileReplyEditSubmit": 112, + "routes.ProfileReplyDeleteSubmit": 113, + "routes.PollVote": 114, + "routes.PollResults": 115, + "routes.AccountLogin": 116, + "routes.AccountRegister": 117, + "routes.AccountLogout": 118, + "routes.AccountLoginSubmit": 119, + "routes.AccountLoginMFAVerify": 120, + "routes.AccountLoginMFAVerifySubmit": 121, + "routes.AccountRegisterSubmit": 122, + "routes.DynamicRoute": 123, + "routes.UploadedFile": 124, + "routes.StaticFile": 125, + "routes.RobotsTxt": 126, + "routes.SitemapXml": 127, + "routes.BadRoute": 128, } var reverseRouteMapEnum = map[int]string{ 0: "routes.Overview", @@ -374,44 +376,45 @@ var reverseRouteMapEnum = map[int]string{ 87: "routes.AccountEditMFADisableSubmit", 88: "routes.AccountEditEmail", 89: "routes.AccountEditEmailTokenSubmit", - 90: "routes.LevelList", - 91: "routes.ViewProfile", - 92: "routes.BanUserSubmit", - 93: "routes.UnbanUser", - 94: "routes.ActivateUser", - 95: "routes.IPSearch", - 96: "routes.CreateTopicSubmit", - 97: "routes.EditTopicSubmit", - 98: "routes.DeleteTopicSubmit", - 99: "routes.StickTopicSubmit", - 100: "routes.UnstickTopicSubmit", - 101: "routes.LockTopicSubmit", - 102: "routes.UnlockTopicSubmit", - 103: "routes.MoveTopicSubmit", - 104: "routes.LikeTopicSubmit", - 105: "routes.ViewTopic", - 106: "routes.CreateReplySubmit", - 107: "routes.ReplyEditSubmit", - 108: "routes.ReplyDeleteSubmit", - 109: "routes.ReplyLikeSubmit", - 110: "routes.ProfileReplyCreateSubmit", - 111: "routes.ProfileReplyEditSubmit", - 112: "routes.ProfileReplyDeleteSubmit", - 113: "routes.PollVote", - 114: "routes.PollResults", - 115: "routes.AccountLogin", - 116: "routes.AccountRegister", - 117: "routes.AccountLogout", - 118: "routes.AccountLoginSubmit", - 119: "routes.AccountLoginMFAVerify", - 120: "routes.AccountLoginMFAVerifySubmit", - 121: "routes.AccountRegisterSubmit", - 122: "routes.DynamicRoute", - 123: "routes.UploadedFile", - 124: "routes.StaticFile", - 125: "routes.RobotsTxt", - 126: "routes.SitemapXml", - 127: "routes.BadRoute", + 90: "routes.AccountLogins", + 91: "routes.LevelList", + 92: "routes.ViewProfile", + 93: "routes.BanUserSubmit", + 94: "routes.UnbanUser", + 95: "routes.ActivateUser", + 96: "routes.IPSearch", + 97: "routes.CreateTopicSubmit", + 98: "routes.EditTopicSubmit", + 99: "routes.DeleteTopicSubmit", + 100: "routes.StickTopicSubmit", + 101: "routes.UnstickTopicSubmit", + 102: "routes.LockTopicSubmit", + 103: "routes.UnlockTopicSubmit", + 104: "routes.MoveTopicSubmit", + 105: "routes.LikeTopicSubmit", + 106: "routes.ViewTopic", + 107: "routes.CreateReplySubmit", + 108: "routes.ReplyEditSubmit", + 109: "routes.ReplyDeleteSubmit", + 110: "routes.ReplyLikeSubmit", + 111: "routes.ProfileReplyCreateSubmit", + 112: "routes.ProfileReplyEditSubmit", + 113: "routes.ProfileReplyDeleteSubmit", + 114: "routes.PollVote", + 115: "routes.PollResults", + 116: "routes.AccountLogin", + 117: "routes.AccountRegister", + 118: "routes.AccountLogout", + 119: "routes.AccountLoginSubmit", + 120: "routes.AccountLoginMFAVerify", + 121: "routes.AccountLoginMFAVerifySubmit", + 122: "routes.AccountRegisterSubmit", + 123: "routes.DynamicRoute", + 124: "routes.UploadedFile", + 125: "routes.StaticFile", + 126: "routes.RobotsTxt", + 127: "routes.SitemapXml", + 128: "routes.BadRoute", } var osMapEnum = map[string]int{ "unknown": 0, @@ -702,7 +705,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { counters.GlobalViewCounter.Bump() if prefix == "/static" { - counters.RouteViewCounter.Bump(124) + counters.RouteViewCounter.Bump(125) req.URL.Path += extraData routes.StaticFile(w, req) return @@ -1567,7 +1570,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c counters.RouteViewCounter.Bump(89) err = routes.AccountEditEmailTokenSubmit(w,req,user,extraData) - case "/user/levels/": + case "/user/edit/logins/": err = common.MemberOnly(w,req,user) if err != nil { return err @@ -1575,13 +1578,25 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c counters.RouteViewCounter.Bump(90) head, err := common.UserCheck(w,req,&user) + if err != nil { + return err + } + err = routes.AccountLogins(w,req,user,head) + case "/user/levels/": + err = common.MemberOnly(w,req,user) + if err != nil { + return err + } + + counters.RouteViewCounter.Bump(91) + head, err := common.UserCheck(w,req,&user) if err != nil { return err } err = routes.LevelList(w,req,user,head) default: req.URL.Path += extraData - counters.RouteViewCounter.Bump(91) + counters.RouteViewCounter.Bump(92) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1601,7 +1616,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(92) + counters.RouteViewCounter.Bump(93) err = routes.BanUserSubmit(w,req,user,extraData) case "/users/unban/": err = common.NoSessionMismatch(w,req,user) @@ -1614,7 +1629,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(93) + counters.RouteViewCounter.Bump(94) err = routes.UnbanUser(w,req,user,extraData) case "/users/activate/": err = common.NoSessionMismatch(w,req,user) @@ -1627,7 +1642,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(94) + counters.RouteViewCounter.Bump(95) err = routes.ActivateUser(w,req,user,extraData) case "/users/ips/": err = common.MemberOnly(w,req,user) @@ -1635,7 +1650,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(95) + counters.RouteViewCounter.Bump(96) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1659,7 +1674,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(96) + counters.RouteViewCounter.Bump(97) err = routes.CreateTopicSubmit(w,req,user) case "/topic/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1672,7 +1687,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(97) + counters.RouteViewCounter.Bump(98) err = routes.EditTopicSubmit(w,req,user,extraData) case "/topic/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1686,7 +1701,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c } req.URL.Path += extraData - counters.RouteViewCounter.Bump(98) + counters.RouteViewCounter.Bump(99) err = routes.DeleteTopicSubmit(w,req,user) case "/topic/stick/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1699,7 +1714,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(99) + counters.RouteViewCounter.Bump(100) err = routes.StickTopicSubmit(w,req,user,extraData) case "/topic/unstick/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1712,7 +1727,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(100) + counters.RouteViewCounter.Bump(101) err = routes.UnstickTopicSubmit(w,req,user,extraData) case "/topic/lock/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1726,7 +1741,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c } req.URL.Path += extraData - counters.RouteViewCounter.Bump(101) + counters.RouteViewCounter.Bump(102) err = routes.LockTopicSubmit(w,req,user) case "/topic/unlock/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1739,7 +1754,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(102) + counters.RouteViewCounter.Bump(103) err = routes.UnlockTopicSubmit(w,req,user,extraData) case "/topic/move/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1752,7 +1767,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(103) + counters.RouteViewCounter.Bump(104) err = routes.MoveTopicSubmit(w,req,user,extraData) case "/topic/like/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1770,10 +1785,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(104) + counters.RouteViewCounter.Bump(105) err = routes.LikeTopicSubmit(w,req,user,extraData) default: - counters.RouteViewCounter.Bump(105) + counters.RouteViewCounter.Bump(106) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1797,7 +1812,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(106) + counters.RouteViewCounter.Bump(107) err = routes.CreateReplySubmit(w,req,user) case "/reply/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1810,7 +1825,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(107) + counters.RouteViewCounter.Bump(108) err = routes.ReplyEditSubmit(w,req,user,extraData) case "/reply/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1823,7 +1838,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(108) + counters.RouteViewCounter.Bump(109) err = routes.ReplyDeleteSubmit(w,req,user,extraData) case "/reply/like/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1841,7 +1856,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(109) + counters.RouteViewCounter.Bump(110) err = routes.ReplyLikeSubmit(w,req,user,extraData) } case "/profile": @@ -1857,7 +1872,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(110) + counters.RouteViewCounter.Bump(111) err = routes.ProfileReplyCreateSubmit(w,req,user) case "/profile/reply/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1870,7 +1885,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(111) + counters.RouteViewCounter.Bump(112) err = routes.ProfileReplyEditSubmit(w,req,user,extraData) case "/profile/reply/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1883,7 +1898,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(112) + counters.RouteViewCounter.Bump(113) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) } case "/poll": @@ -1899,23 +1914,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(113) + counters.RouteViewCounter.Bump(114) err = routes.PollVote(w,req,user,extraData) case "/poll/results/": - counters.RouteViewCounter.Bump(114) + counters.RouteViewCounter.Bump(115) err = routes.PollResults(w,req,user,extraData) } case "/accounts": switch(req.URL.Path) { case "/accounts/login/": - counters.RouteViewCounter.Bump(115) + counters.RouteViewCounter.Bump(116) head, err := common.UserCheck(w,req,&user) if err != nil { return err } err = routes.AccountLogin(w,req,user,head) case "/accounts/create/": - counters.RouteViewCounter.Bump(116) + counters.RouteViewCounter.Bump(117) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1932,7 +1947,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(117) + counters.RouteViewCounter.Bump(118) err = routes.AccountLogout(w,req,user) case "/accounts/login/submit/": err = common.ParseForm(w,req,user) @@ -1940,10 +1955,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(118) + counters.RouteViewCounter.Bump(119) err = routes.AccountLoginSubmit(w,req,user) case "/accounts/mfa_verify/": - counters.RouteViewCounter.Bump(119) + counters.RouteViewCounter.Bump(120) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1955,7 +1970,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(120) + counters.RouteViewCounter.Bump(121) err = routes.AccountLoginMFAVerifySubmit(w,req,user) case "/accounts/create/submit/": err = common.ParseForm(w,req,user) @@ -1963,7 +1978,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(121) + counters.RouteViewCounter.Bump(122) err = routes.AccountRegisterSubmit(w,req,user) } /*case "/sitemaps": // TODO: Count these views @@ -1979,7 +1994,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(123) + counters.RouteViewCounter.Bump(124) req.URL.Path += extraData // TODO: Find a way to propagate errors up from this? r.UploadHandler(w,req) // TODO: Count these views @@ -1989,10 +2004,10 @@ 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(125) + counters.RouteViewCounter.Bump(126) return routes.RobotsTxt(w,req) /*case "sitemap.xml": - counters.RouteViewCounter.Bump(126) + counters.RouteViewCounter.Bump(127) return routes.SitemapXml(w,req)*/ } return common.NotFound(w,req,nil) @@ -2003,7 +2018,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c r.RUnlock() if ok { - counters.RouteViewCounter.Bump(122) // TODO: Be more specific about *which* dynamic route it is + counters.RouteViewCounter.Bump(123) // TODO: Be more specific about *which* dynamic route it is req.URL.Path += extraData return handle(w,req,user) } @@ -2014,7 +2029,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c } else { r.DumpRequest(req,"Bad Route") } - counters.RouteViewCounter.Bump(127) + counters.RouteViewCounter.Bump(128) return common.NotFound(w,req,nil) } return err diff --git a/gen_tables.go b/gen_tables.go index d26d8f33..e30a6664 100644 --- a/gen_tables.go +++ b/gen_tables.go @@ -2,22 +2,23 @@ package main var dbTablePrimaryKeys = map[string]string{ - "attachments":"attachID", + "users":"uid", + "users_groups":"gid", + "users_groups_scheduler":"uid", + "polls":"pollID", + "registration_logs":"rlid", + "activity_stream":"asid", + "login_logs":"lid", + "users_2fa_keys":"uid", "topics":"tid", "replies":"rid", - "polls":"pollID", - "users_2fa_keys":"uid", - "users_groups_scheduler":"uid", + "attachments":"attachID", "revisions":"reviseID", - "users_replies":"rid", - "menus":"mid", - "users":"uid", - "forums":"fid", - "activity_stream":"asid", - "word_filters":"wfid", - "menu_items":"miid", - "pages":"pid", - "registration_logs":"rlid", - "users_groups":"gid", "users_avatar_queue":"uid", + "forums":"fid", + "menu_items":"miid", + "users_replies":"rid", + "word_filters":"wfid", + "menus":"mid", + "pages":"pid", } diff --git a/general_test.go b/general_test.go index f5b837d0..f45907f1 100644 --- a/general_test.go +++ b/general_test.go @@ -175,11 +175,12 @@ func BenchmarkTopicAdminRouteParallelWithRouter(b *testing.B) { } uidCookie := http.Cookie{Name: "uid", Value: "1", Path: "/", MaxAge: common.Year} sessionCookie := http.Cookie{Name: "session", Value: admin.Session, Path: "/", MaxAge: common.Year} + path := "/topic/hm."+benchTid b.RunParallel(func(pb *testing.PB) { for pb.Next() { w := httptest.NewRecorder() - reqAdmin := httptest.NewRequest("get", "/topic/hm."+benchTid, bytes.NewReader(nil)) + reqAdmin := httptest.NewRequest("get", path, bytes.NewReader(nil)) reqAdmin.AddCookie(&uidCookie) reqAdmin.AddCookie(&sessionCookie) reqAdmin.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36") @@ -209,6 +210,60 @@ func BenchmarkTopicAdminRouteParallelAltAlt(b *testing.B) { BenchmarkTopicAdminRouteParallel(b) } +func BenchmarkTopicGuestAdminRouteParallelWithRouter(b *testing.B) { + binit(b) + router, err := NewGenRouter(http.FileServer(http.Dir("./uploads"))) + if err != nil { + b.Fatal(err) + } + cfg := NewStashConfig() + common.Dev.DebugMode = false + common.Dev.SuperDebug = false + + admin, err := common.Users.Get(1) + if err != nil { + b.Fatal(err) + } + if !admin.IsAdmin { + b.Fatal("UID1 is not an admin") + } + uidCookie := http.Cookie{Name: "uid", Value: "1", Path: "/", MaxAge: common.Year} + sessionCookie := http.Cookie{Name: "session", Value: admin.Session, Path: "/", MaxAge: common.Year} + path := "/topic/hm."+benchTid + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + w := httptest.NewRecorder() + reqAdmin := httptest.NewRequest("get", path, bytes.NewReader(nil)) + reqAdmin.AddCookie(&uidCookie) + reqAdmin.AddCookie(&sessionCookie) + reqAdmin.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36") + reqAdmin.Header.Set("Host", "localhost") + reqAdmin.Host = "localhost" + router.ServeHTTP(w, reqAdmin) + if w.Code != 200 { + b.Log(w.Body) + b.Fatal("HTTP Error!") + } + +{ + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", path, bytes.NewReader(nil)) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36") + req.Header.Set("Host", "localhost") + req.Host = "localhost" + router.ServeHTTP(w, req) + if w.Code != 200 { + b.Log(w.Body) + b.Fatal("HTTP Error!") + } +} + } + }) + + cfg.Restore() +} + func BenchmarkTopicGuestRouteParallel(b *testing.B) { binit(b) cfg := NewStashConfig() @@ -272,6 +327,15 @@ func obRoute(b *testing.B, path string) { cfg.Restore() } +func obRouteNoError(b *testing.B, path string) { + binit(b) + cfg := NewStashConfig() + common.Dev.DebugMode = false + common.Dev.SuperDebug = false + b.RunParallel(benchRouteNoError(b, path)) + cfg.Restore() +} + func BenchmarkTopicsGuestRouteParallelWithRouter(b *testing.B) { obRoute(b, "/topics/") } @@ -292,10 +356,9 @@ func BenchmarkTopicGuestRouteParallelWithRouterAlt(b *testing.B) { obRoute(b, "/topic/hm."+benchTid) } -// TODO: Needs to stop failing the tests unnecessarily -/*func BenchmarkBadRouteGuestRouteParallelWithRouter(b *testing.B) { - obRoute(b, "/garble/haa") -}*/ +func BenchmarkBadRouteGuestRouteParallelWithRouter(b *testing.B) { + obRouteNoError(b, "/garble/haa") +} // TODO: Alternate between member and guest to bust some CPU caches? @@ -330,20 +393,37 @@ func benchRoute(b *testing.B, path string) func(*testing.PB) { } return func(pb *testing.PB) { for pb.Next() { - listW := httptest.NewRecorder() - listReq := httptest.NewRequest("GET", path, bytes.NewReader(nil)) - listReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36") - listReq.Header.Set("Host", "localhost") - listReq.Host = "localhost" - router.ServeHTTP(listW, listReq) - if listW.Code != 200 { - b.Log(listW.Body) + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", path, bytes.NewReader(nil)) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36") + req.Header.Set("Host", "localhost") + req.Host = "localhost" + router.ServeHTTP(w, req) + if w.Code != 200 { + b.Log(w.Body) b.Fatal("HTTP Error!") } } } } +func benchRouteNoError(b *testing.B, path string) func(*testing.PB) { + router, err := NewGenRouter(http.FileServer(http.Dir("./uploads"))) + if err != nil { + b.Fatal(err) + } + return func(pb *testing.PB) { + for pb.Next() { + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", path, bytes.NewReader(nil)) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36") + req.Header.Set("Host", "localhost") + req.Host = "localhost" + router.ServeHTTP(w, req) + } + } +} + func BenchmarkProfileGuestRouteParallelWithRouter(b *testing.B) { obRoute(b, "/profile/admin.1") } diff --git a/langs/english.json b/langs/english.json index a42daf59..6ff4ecfa 100644 --- a/langs/english.json +++ b/langs/english.json @@ -132,6 +132,7 @@ "account_mfa":"Manage 2FA", "account_mfa_setup":"Setup 2FA", "account_email":"Email Manager", + "account_logins":"Logins", "account_level_list":"Level Progress", "panel_dashboard":"Control Panel Dashboard", @@ -447,6 +448,7 @@ "account_menu_email":"Email", "account_menu_security":"Security", "account_menu_notifications":"Notifications", + "account_menu_logins":"Logins", "account_coming_soon":"Coming Soon", @@ -480,6 +482,10 @@ "account_mfa_setup_verify":"Verify", "account_mfa_setup_button":"Setup", + "account_logins_head":"Logins", + "account_logins_success":"Successful Login", + "account_logins_failure":"Failed Login", + "areyousure_head":"Are you sure?", "areyousure_continue":"Continue", diff --git a/main.go b/main.go index 0ef5e787..2e43cf94 100644 --- a/main.go +++ b/main.go @@ -125,6 +125,10 @@ func afterDBInit() (err error) { if err != nil { return errors.WithStack(err) } + common.LoginLogs, err = common.NewLoginLogStore(acc) + if err != nil { + return errors.WithStack(err) + } common.RegLogs, err = common.NewRegLogStore(acc) if err != nil { return errors.WithStack(err) diff --git a/patcher/patches.go b/patcher/patches.go index 2896bbfe..03930ac0 100644 --- a/patcher/patches.go +++ b/patcher/patches.go @@ -17,6 +17,7 @@ func init() { addPatch(6, patch6) addPatch(7, patch7) addPatch(8, patch8) + addPatch(9, patch9) } func patch0(scanner *bufio.Scanner) (err error) { @@ -134,9 +135,9 @@ func patch0(scanner *bufio.Scanner) (err error) { func patch1(scanner *bufio.Scanner) error { var routes = map[string]string{ "routeAccountEditCriticalSubmit": "routes.AccountEditCriticalSubmit", - "routeAccountEditAvatar": "routes.AccountEditAvatar", - "routeAccountEditAvatarSubmit": "routes.AccountEditAvatarSubmit", - "routeAccountEditUsername": "routes.AccountEditUsername", + "routeAccountEditAvatar": "routes.AccountEditAvatar", + "routeAccountEditAvatarSubmit": "routes.AccountEditAvatarSubmit", + "routeAccountEditUsername": "routes.AccountEditUsername", "routeAccountEditUsernameSubmit": "routes.AccountEditUsernameSubmit", } return renameRoutes(routes) @@ -144,15 +145,15 @@ func patch1(scanner *bufio.Scanner) error { func patch2(scanner *bufio.Scanner) error { var routes = map[string]string{ - "routeLogout": "routes.AccountLogout", - "routeShowAttachment": "routes.ShowAttachment", - "routeChangeTheme": "routes.ChangeTheme", + "routeLogout": "routes.AccountLogout", + "routeShowAttachment": "routes.ShowAttachment", + "routeChangeTheme": "routes.ChangeTheme", "routeProfileReplyCreateSubmit": "routes.ProfileReplyCreateSubmit", - "routeLikeTopicSubmit": "routes.LikeTopicSubmit", - "routeReplyLikeSubmit": "routes.ReplyLikeSubmit", - "routeDynamic": "routes.DynamicRoute", - "routeUploads": "routes.UploadedFile", - "BadRoute": "routes.BadRoute", + "routeLikeTopicSubmit": "routes.LikeTopicSubmit", + "routeReplyLikeSubmit": "routes.ReplyLikeSubmit", + "routeDynamic": "routes.DynamicRoute", + "routeUploads": "routes.UploadedFile", + "BadRoute": "routes.BadRoute", } return renameRoutes(routes) } @@ -181,41 +182,41 @@ func patch3(scanner *bufio.Scanner) error { func patch4(scanner *bufio.Scanner) error { var routes = map[string]string{ - "routeReportSubmit": "routes.ReportSubmit", - "routeAccountEditEmail": "routes.AccountEditEmail", - "routeAccountEditEmailTokenSubmit": "routes.AccountEditEmailTokenSubmit", - "routePanelLogsRegs": "panel.LogsRegs", - "routePanelLogsMod": "panel.LogsMod", - "routePanelLogsAdmin": "panel.LogsAdmin", - "routePanelDebug": "panel.Debug", - "routePanelAnalyticsViews": "panel.AnalyticsViews", - "routePanelAnalyticsRouteViews": "panel.AnalyticsRouteViews", - "routePanelAnalyticsAgentViews": "panel.AnalyticsAgentViews", - "routePanelAnalyticsForumViews": "panel.AnalyticsForumViews", - "routePanelAnalyticsSystemViews": "panel.AnalyticsSystemViews", - "routePanelAnalyticsLanguageViews": "panel.AnalyticsLanguageViews", - "routePanelAnalyticsReferrerViews": "panel.AnalyticsReferrerViews", - "routePanelAnalyticsTopics": "panel.AnalyticsTopics", - "routePanelAnalyticsPosts": "panel.AnalyticsPosts", - "routePanelAnalyticsForums": "panel.AnalyticsForums", - "routePanelAnalyticsRoutes": "panel.AnalyticsRoutes", - "routePanelAnalyticsAgents": "panel.AnalyticsAgents", - "routePanelAnalyticsSystems": "panel.AnalyticsSystems", - "routePanelAnalyticsLanguages": "panel.AnalyticsLanguages", - "routePanelAnalyticsReferrers": "panel.AnalyticsReferrers", - "routePanelSettings": "panel.Settings", - "routePanelSettingEdit": "panel.SettingEdit", - "routePanelSettingEditSubmit": "panel.SettingEditSubmit", - "routePanelForums": "panel.Forums", - "routePanelForumsCreateSubmit": "panel.ForumsCreateSubmit", - "routePanelForumsDelete": "panel.ForumsDelete", - "routePanelForumsDeleteSubmit": "panel.ForumsDeleteSubmit", - "routePanelForumsEdit": "panel.ForumsEdit", - "routePanelForumsEditSubmit": "panel.ForumsEditSubmit", - "routePanelForumsEditPermsSubmit": "panel.ForumsEditPermsSubmit", - "routePanelForumsEditPermsAdvance": "panel.ForumsEditPermsAdvance", + "routeReportSubmit": "routes.ReportSubmit", + "routeAccountEditEmail": "routes.AccountEditEmail", + "routeAccountEditEmailTokenSubmit": "routes.AccountEditEmailTokenSubmit", + "routePanelLogsRegs": "panel.LogsRegs", + "routePanelLogsMod": "panel.LogsMod", + "routePanelLogsAdmin": "panel.LogsAdmin", + "routePanelDebug": "panel.Debug", + "routePanelAnalyticsViews": "panel.AnalyticsViews", + "routePanelAnalyticsRouteViews": "panel.AnalyticsRouteViews", + "routePanelAnalyticsAgentViews": "panel.AnalyticsAgentViews", + "routePanelAnalyticsForumViews": "panel.AnalyticsForumViews", + "routePanelAnalyticsSystemViews": "panel.AnalyticsSystemViews", + "routePanelAnalyticsLanguageViews": "panel.AnalyticsLanguageViews", + "routePanelAnalyticsReferrerViews": "panel.AnalyticsReferrerViews", + "routePanelAnalyticsTopics": "panel.AnalyticsTopics", + "routePanelAnalyticsPosts": "panel.AnalyticsPosts", + "routePanelAnalyticsForums": "panel.AnalyticsForums", + "routePanelAnalyticsRoutes": "panel.AnalyticsRoutes", + "routePanelAnalyticsAgents": "panel.AnalyticsAgents", + "routePanelAnalyticsSystems": "panel.AnalyticsSystems", + "routePanelAnalyticsLanguages": "panel.AnalyticsLanguages", + "routePanelAnalyticsReferrers": "panel.AnalyticsReferrers", + "routePanelSettings": "panel.Settings", + "routePanelSettingEdit": "panel.SettingEdit", + "routePanelSettingEditSubmit": "panel.SettingEditSubmit", + "routePanelForums": "panel.Forums", + "routePanelForumsCreateSubmit": "panel.ForumsCreateSubmit", + "routePanelForumsDelete": "panel.ForumsDelete", + "routePanelForumsDeleteSubmit": "panel.ForumsDeleteSubmit", + "routePanelForumsEdit": "panel.ForumsEdit", + "routePanelForumsEditSubmit": "panel.ForumsEditSubmit", + "routePanelForumsEditPermsSubmit": "panel.ForumsEditPermsSubmit", + "routePanelForumsEditPermsAdvance": "panel.ForumsEditPermsAdvance", "routePanelForumsEditPermsAdvanceSubmit": "panel.ForumsEditPermsAdvanceSubmit", - "routePanelBackups": "panel.Backups", + "routePanelBackups": "panel.Backups", } err := renameRoutes(routes) if err != nil { @@ -249,10 +250,10 @@ func patch4(scanner *bufio.Scanner) error { func patch5(scanner *bufio.Scanner) error { var routes = map[string]string{ - "routePanelUsers": "panel.Users", - "routePanelUsersEdit": "panel.UsersEdit", - "routePanelUsersEditSubmit": "panel.UsersEditSubmit", - "routes.AccountEditCritical": "routes.AccountEditPassword", + "routePanelUsers": "panel.Users", + "routePanelUsersEdit": "panel.UsersEdit", + "routePanelUsersEditSubmit": "panel.UsersEditSubmit", + "routes.AccountEditCritical": "routes.AccountEditPassword", "routes.AccountEditCriticalSubmit": "routes.AccountEditPasswordSubmit", } err := renameRoutes(routes) @@ -322,7 +323,7 @@ func renameRoutes(routes map[string]string) error { } for key, value := range routes { - err := replaceTextWhere(key,value) + err := replaceTextWhere(key, value) if err != nil { return err } @@ -333,37 +334,41 @@ func renameRoutes(routes map[string]string) error { func patch8(scanner *bufio.Scanner) error { var routes = map[string]string{ - "routePanelWordFilter": "panel.WordFilters", - "routePanelWordFiltersCreateSubmit": "panel.WordFiltersCreateSubmit", - "routePanelWordFiltersEdit": "panel.WordFiltersEdit", - "routePanelWordFiltersEditSubmit": "panel.WordFiltersEditSubmit", - "routePanelWordFiltersDeleteSubmit": "panel.WordFiltersDeleteSubmit", - "routePanelPlugins": "panel.Plugins", - "routePanelPluginsActivate": "panel.PluginsActivate", - "routePanelPluginsDeactivate": "panel.PluginsDeactivate", - "routePanelPluginsInstall": "panel.PluginsInstall", - "routePanelGroups": "panel.Groups", - "routePanelGroupsEdit":"panel.GroupsEdit", - "routePanelGroupsEditPerms":"panel.GroupsEditPerms", - "routePanelGroupsEditSubmit":"panel.GroupsEditSubmit", - "routePanelGroupsEditPermsSubmit":"panel.GroupsEditPermsSubmit", - "routePanelGroupsCreateSubmit":"panel.GroupsCreateSubmit", - "routePanelThemes":"panel.Themes", - "routePanelThemesSetDefault":"panel.ThemesSetDefault", - "routePanelThemesMenus":"panel.ThemesMenus", - "routePanelThemesMenusEdit":"panel.ThemesMenusEdit", - "routePanelThemesMenuItemEdit":"panel.ThemesMenuItemEdit", - "routePanelThemesMenuItemEditSubmit":"panel.ThemesMenuItemEditSubmit", - "routePanelThemesMenuItemCreateSubmit":"panel.ThemesMenuItemCreateSubmit", - "routePanelThemesMenuItemDeleteSubmit":"panel.ThemesMenuItemDeleteSubmit", - "routePanelThemesMenuItemOrderSubmit":"panel.ThemesMenuItemOrderSubmit", - "routePanelDashboard":"panel.Dashboard", + "routePanelWordFilter": "panel.WordFilters", + "routePanelWordFiltersCreateSubmit": "panel.WordFiltersCreateSubmit", + "routePanelWordFiltersEdit": "panel.WordFiltersEdit", + "routePanelWordFiltersEditSubmit": "panel.WordFiltersEditSubmit", + "routePanelWordFiltersDeleteSubmit": "panel.WordFiltersDeleteSubmit", + "routePanelPlugins": "panel.Plugins", + "routePanelPluginsActivate": "panel.PluginsActivate", + "routePanelPluginsDeactivate": "panel.PluginsDeactivate", + "routePanelPluginsInstall": "panel.PluginsInstall", + "routePanelGroups": "panel.Groups", + "routePanelGroupsEdit": "panel.GroupsEdit", + "routePanelGroupsEditPerms": "panel.GroupsEditPerms", + "routePanelGroupsEditSubmit": "panel.GroupsEditSubmit", + "routePanelGroupsEditPermsSubmit": "panel.GroupsEditPermsSubmit", + "routePanelGroupsCreateSubmit": "panel.GroupsCreateSubmit", + "routePanelThemes": "panel.Themes", + "routePanelThemesSetDefault": "panel.ThemesSetDefault", + "routePanelThemesMenus": "panel.ThemesMenus", + "routePanelThemesMenusEdit": "panel.ThemesMenusEdit", + "routePanelThemesMenuItemEdit": "panel.ThemesMenuItemEdit", + "routePanelThemesMenuItemEditSubmit": "panel.ThemesMenuItemEditSubmit", + "routePanelThemesMenuItemCreateSubmit": "panel.ThemesMenuItemCreateSubmit", + "routePanelThemesMenuItemDeleteSubmit": "panel.ThemesMenuItemDeleteSubmit", + "routePanelThemesMenuItemOrderSubmit": "panel.ThemesMenuItemOrderSubmit", + "routePanelDashboard": "panel.Dashboard", } err := renameRoutes(routes) if err != nil { return err } + err = execStmt(qgen.Builder.DropTable("updates")) + if err != nil { + return err + } err = execStmt(qgen.Builder.CreateTable("updates", "", "", []qgen.DBTableColumn{ qgen.DBTableColumn{"dbVersion", "int", 0, false, false, "0"}, @@ -376,3 +381,29 @@ func patch8(scanner *bufio.Scanner) error { return nil } + +func patch9(scanner *bufio.Scanner) error { + // Table "updates" might not exist due to the installer, so drop it and remake it if so + err := patch8(scanner) + if err != nil { + return err + } + + err = execStmt(qgen.Builder.CreateTable("login_logs", "", "", + []qgen.DBTableColumn{ + qgen.DBTableColumn{"lid", "int", 0, false, true, ""}, + qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, + qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? + qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, + qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""}, + }, + []qgen.DBTableKey{ + qgen.DBTableKey{"lid", "primary"}, + }, + )) + if err != nil { + return err + } + + return nil +} diff --git a/router_gen/routes.go b/router_gen/routes.go index cd684132..1d97b79d 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -61,6 +61,8 @@ func userRoutes() *RouteGroup { MemberView("routes.AccountEditEmail", "/user/edit/email/"), Action("routes.AccountEditEmailTokenSubmit", "/user/edit/token/", "extraData"), + MemberView("routes.AccountLogins", "/user/edit/logins/"), + MemberView("routes.LevelList", "/user/levels/"), //MemberView("routes.LevelRankings", "/user/rankings/"), ) diff --git a/routes/account.go b/routes/account.go index d23a87b2..6935466c 100644 --- a/routes/account.go +++ b/routes/account.go @@ -42,14 +42,31 @@ func AccountLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User username := common.SanitiseSingleLine(r.PostFormValue("username")) uid, err, requiresExtraAuth := common.Auth.Authenticate(username, r.PostFormValue("password")) if err != nil { + { + // TODO: uid is currently set to 0 as authenticate fetches the user by username and password. Get the actual uid, so we can alert the user of attempted logins? What if someone takes advantage of the response times to deduce if an account exists? + logItem := &common.LoginLogItem{UID: uid, Success: false, IPAddress: user.LastIP} + _, err := logItem.Create() + if err != nil { + return common.InternalError(err, w, r) + } + } return common.LocalError(err.Error(), w, r, user) } + + // TODO: Take 2FA into account + logItem := &common.LoginLogItem{UID: uid, Success: true, IPAddress: user.LastIP} + _, err = logItem.Create() + if err != nil { + return common.InternalError(err, w, r) + } + // TODO: Do we want to slacken this by only doing it when the IP changes? if requiresExtraAuth { provSession, signedSession, err := common.Auth.CreateProvisionalSession(uid) if err != nil { return common.InternalError(err, w, r) } + // TODO: Use the login log ID in the provisional cookie? common.Auth.SetProvisionalCookies(w, uid, provSession, signedSession) http.Redirect(w, r, "/accounts/mfa_verify/", http.StatusSeeOther) return nil @@ -365,7 +382,7 @@ func AccountEdit(w http.ResponseWriter, r *http.Request, user common.User, heade nextScore := common.GetLevelScore(user.Level+1) - prevScore perc := int(math.Ceil((float64(nextScore) / float64(currentScore)) * 100)) - pi := common.AccountDashPage{header, "dashboard", "account_own_edit", mfaSetup, currentScore, nextScore, user.Level + 1, perc * 2} + pi := common.Account{header, "dashboard", "account_own_edit", common.AccountDashPage{header, mfaSetup, currentScore, nextScore, user.Level + 1, perc * 2}} return renderTemplate("account", w, r, header, pi) } @@ -526,14 +543,7 @@ func AccountEditMFA(w http.ResponseWriter, r *http.Request, user common.User, he } pi := common.Page{header, tList, mfaItem.Scratch} - if common.RunPreRenderHook("pre_render_account_own_edit_mfa", w, r, &user, &pi) { - return nil - } - err = common.Templates.ExecuteTemplate(w, "account_own_edit_mfa.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - return nil + return renderTemplate("account_own_edit_mfa", w, r, header, pi) } // If not setup, generate a string, otherwise give an option to disable mfa given the right code @@ -555,14 +565,7 @@ func AccountEditMFASetup(w http.ResponseWriter, r *http.Request, user common.Use } pi := common.Page{header, tList, common.FriendlyGAuthSecret(code)} - if common.RunPreRenderHook("pre_render_account_own_edit_mfa_setup", w, r, &user, &pi) { - return nil - } - err = common.Templates.ExecuteTemplate(w, "account_own_edit_mfa_setup.html", pi) - if err != nil { - return common.InternalError(err, w, r) - } - return nil + return renderTemplate("account_own_edit_mfa_setup", w, r, header, pi) } // Form should bounce the random mfa secret back and the otp to be verified server-side to reduce the chances of a bug arising on the JS side which makes every code mismatch @@ -650,8 +653,8 @@ func AccountEditEmail(w http.ResponseWriter, r *http.Request, user common.User, header.AddNotice("account_mail_verify_success") } - pi := common.EmailListPage{header, emails, nil} - return renderTemplate("account_own_edit_email", w, r, header, pi) + pi := common.Account{header, "edit_emails", "account_own_edit_email", common.EmailListPage{header, emails}} + return renderTemplate("account", w, r, header, pi) } // TODO: Should we make this an AnonAction so someone can do this without being logged in? @@ -699,6 +702,24 @@ func AccountEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, user co return nil } +func AccountLogins(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header) common.RouteError { + accountEditHead("account_logins", w, r, &user, header) + + logCount := common.LoginLogs.GlobalCount() + page, _ := strconv.Atoi(r.FormValue("page")) + perPage := 12 + offset, page, lastPage := common.PageOffset(logCount, page, perPage) + + logs, err := common.LoginLogs.GetOffset(user.ID, offset, perPage) + if err != nil { + return common.InternalError(err, w, r) + } + + pageList := common.Paginate(logCount, perPage, 5) + pi := common.Account{header, "logins", "account_logins", common.AccountLoginsPage{header, logs, common.Paginator{pageList, page, lastPage}}} + return renderTemplate("account", w, r, header, pi) +} + func LevelList(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header) common.RouteError { header.Title = phrases.GetTitlePhrase("account_level_list") diff --git a/routes/forum.go b/routes/forum.go index 3e639a46..5e1e0353 100644 --- a/routes/forum.go +++ b/routes/forum.go @@ -75,6 +75,9 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, header topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID) topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt) + // TODO: Create a specialised function with a bit less overhead for getting the last page for a post count + _, _, lastPage := common.PageOffset(topicItem.PostCount, 1, common.Config.ItemsPerPage) + topicItem.LastPage = lastPage header.Hooks.VhookNoRet("forum_trow_assign", &topicItem, &forum) topicList = append(topicList, &topicItem) diff --git a/routes/topic.go b/routes/topic.go index 259e553f..a8e12b5a 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -107,7 +107,8 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header // Calculate the offset offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage) - tpage := common.TopicPage{header, []common.ReplyUser{}, topic, forum, poll, page, lastPage} + pageList := common.Paginate(topic.PostCount, common.Config.ItemsPerPage, 5) + tpage := common.TopicPage{header, []common.ReplyUser{}, topic, forum, poll, common.Paginator{pageList, page, lastPage}} // Get the replies if we have any... if topic.PostCount > 0 { diff --git a/schema/mssql/query_login_logs.sql b/schema/mssql/query_login_logs.sql new file mode 100644 index 00000000..f5379017 --- /dev/null +++ b/schema/mssql/query_login_logs.sql @@ -0,0 +1,8 @@ +CREATE TABLE [login_logs] ( + [lid] int not null IDENTITY, + [uid] int not null, + [success] bool DEFAULT 0 not null, + [ipaddress] nvarchar (200) not null, + [doneAt] datetime not null, + primary key([lid]) +); \ No newline at end of file diff --git a/schema/mssql/query_updates.sql b/schema/mssql/query_updates.sql new file mode 100644 index 00000000..1a4c5d59 --- /dev/null +++ b/schema/mssql/query_updates.sql @@ -0,0 +1,3 @@ +CREATE TABLE [updates] ( + [dbVersion] int DEFAULT 0 not null +); \ No newline at end of file diff --git a/schema/mysql/query_login_logs.sql b/schema/mysql/query_login_logs.sql new file mode 100644 index 00000000..58a4364b --- /dev/null +++ b/schema/mysql/query_login_logs.sql @@ -0,0 +1,8 @@ +CREATE TABLE `login_logs` ( + `lid` int not null AUTO_INCREMENT, + `uid` int not null, + `success` bool DEFAULT 0 not null, + `ipaddress` varchar(200) not null, + `doneAt` datetime not null, + primary key(`lid`) +); \ No newline at end of file diff --git a/schema/mysql/query_updates.sql b/schema/mysql/query_updates.sql new file mode 100644 index 00000000..733e9976 --- /dev/null +++ b/schema/mysql/query_updates.sql @@ -0,0 +1,3 @@ +CREATE TABLE `updates` ( + `dbVersion` int DEFAULT 0 not null +); \ No newline at end of file diff --git a/schema/pgsql/query_login_logs.sql b/schema/pgsql/query_login_logs.sql new file mode 100644 index 00000000..7bf96551 --- /dev/null +++ b/schema/pgsql/query_login_logs.sql @@ -0,0 +1,8 @@ +CREATE TABLE "login_logs" ( + `lid` serial not null, + `uid` int not null, + `success` bool DEFAULT 0 not null, + `ipaddress` varchar (200) not null, + `doneAt` timestamp not null, + primary key(`lid`) +); \ No newline at end of file diff --git a/schema/pgsql/query_updates.sql b/schema/pgsql/query_updates.sql new file mode 100644 index 00000000..5b968c48 --- /dev/null +++ b/schema/pgsql/query_updates.sql @@ -0,0 +1,3 @@ +CREATE TABLE "updates" ( + `dbVersion` int DEFAULT 0 not null +); \ No newline at end of file diff --git a/schema/schema.json b/schema/schema.json index b3d1f1d4..2829b149 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -1,5 +1,5 @@ { - "DBVersion":"9", + "DBVersion":"10", "DynamicFileVersion":"0", "MinGoVersion":"1.11", "MinVersion":"" diff --git a/templates/account.html b/templates/account.html index 627c83e1..57bdb57c 100644 --- a/templates/account.html +++ b/templates/account.html @@ -1,6 +1,6 @@ {{template "header.html" . }}
{{template "account_menu.html" . }} -
{{dyntmpl .TmplName . .Header}}
+
{{dyntmpl .TmplName .Inner .Header}}
{{template "footer.html" . }} \ No newline at end of file diff --git a/templates/account_logins.html b/templates/account_logins.html new file mode 100644 index 00000000..25925d5b --- /dev/null +++ b/templates/account_logins.html @@ -0,0 +1,20 @@ +
+

{{lang "account_logins_head"}}

+
+
+ + {{range .ItemList}} +
+ + {{if .Success}}{{lang "account_logins_success"}}{{else}}{{lang "account_logins_failure"}}"{{end}}
+ {{.IPAddress}} +
+ + {{.DoneAt}} + +
+ {{end}} +
+{{if gt .LastPage 1}} +{{template "paginator.html" . }} +{{end}} \ No newline at end of file diff --git a/templates/account_menu.html b/templates/account_menu.html index 3a955d8c..d4dbb4d6 100644 --- a/templates/account_menu.html +++ b/templates/account_menu.html @@ -8,6 +8,7 @@
{{lang "account_menu_password"}}
{{lang "account_menu_email"}}
+
{{lang "account_menu_logins"}}
{{/** TODO: Add an alerts page with pagination to go through alerts which either don't fit in the alerts drop-down or which have already been dismissed. Bear in mind though that dismissed alerts older than two weeks might be purged to save space and to speed up the database **/}} diff --git a/templates/account_own_edit_email.html b/templates/account_own_edit_email.html index 4f18e09d..96639c00 100644 --- a/templates/account_own_edit_email.html +++ b/templates/account_own_edit_email.html @@ -1,22 +1,15 @@ -{{template "header.html" . }} -
- {{template "account_menu.html" . }} -
-
-

{{lang "account_email_head"}}

-
-
- - {{range .ItemList}} -
- {{.Email}} - - {{if .Primary}}{{lang "account_email_primary"}}{{else}}{{lang "account_email_secondary"}}{{end}} - {{if .Validated}}{{lang "account_email_verified"}}{{else}}{{lang "account_email_resend_email"}}{{end}} - -
- {{end}} -
-
+
+

{{lang "account_email_head"}}

-{{template "footer.html" . }} +
+ + {{range .ItemList}} +
+ {{.Email}} + + {{if .Primary}}{{lang "account_email_primary"}}{{else}}{{lang "account_email_secondary"}}{{end}} + {{if .Validated}}{{lang "account_email_verified"}}{{else}}{{lang "account_email_resend_email"}}{{end}} + +
+ {{end}} +
\ No newline at end of file diff --git a/templates/forum.html b/templates/forum.html index 3d514412..dd35d1ee 100644 --- a/templates/forum.html +++ b/templates/forum.html @@ -105,7 +105,7 @@ {{.LastUser.Name}}'s Avatar {{.LastUser.Name}}
- {{.RelativeLastReplyAt}} + {{.RelativeLastReplyAt}}
diff --git a/templates/panel_adminlogs.html b/templates/panel_adminlogs.html index e45febbe..ed0b7a47 100644 --- a/templates/panel_adminlogs.html +++ b/templates/panel_adminlogs.html @@ -10,7 +10,7 @@
{{.Action}} - {{if $.CurrentUser.Perms.ViewIPs}}
{{.IPAddress}}{{end}} + {{if $.CurrentUser.Perms.ViewIPs}}
{{.IPAddress}}{{end}}
{{.DoneAt}} diff --git a/templates/panel_modlogs.html b/templates/panel_modlogs.html index ef5bbed2..5d9c7aa0 100644 --- a/templates/panel_modlogs.html +++ b/templates/panel_modlogs.html @@ -10,7 +10,7 @@
{{.Action}} - {{if $.CurrentUser.Perms.ViewIPs}}
{{.IPAddress}}{{end}} + {{if $.CurrentUser.Perms.ViewIPs}}
{{.IPAddress}}{{end}}
{{.DoneAt}} diff --git a/templates/panel_reglogs.html b/templates/panel_reglogs.html index 679865a3..be86c2a9 100644 --- a/templates/panel_reglogs.html +++ b/templates/panel_reglogs.html @@ -10,7 +10,7 @@
{{if not .Success}}{{lang "panel_logs_registration_attempt"}}: {{end}}{{.Username}} ({{lang "panel_logs_registration_email"}}: {{.Email}}){{if .ParsedReason}} ({{lang "panel_logs_registration_reason"}}: {{.ParsedReason}}){{end}} - {{if $.CurrentUser.Perms.ViewIPs}}
{{.IPAddress}}{{end}} + {{if $.CurrentUser.Perms.ViewIPs}}
{{.IPAddress}}{{end}} {{.DoneAt}} diff --git a/templates/topic_alt.html b/templates/topic_alt.html index b3d97d1a..9b4a536b 100644 --- a/templates/topic_alt.html +++ b/templates/topic_alt.html @@ -103,6 +103,9 @@ {{template "topic_alt_posts.html" . }}
+{{if gt .LastPage 1}} +{{template "paginator.html" . }} +{{end}} {{if .CurrentUser.Loggedin}} {{if .CurrentUser.Perms.CreateReply}} diff --git a/templates/topics_topic.html b/templates/topics_topic.html index 050db1a7..fbdc0a5a 100644 --- a/templates/topics_topic.html +++ b/templates/topics_topic.html @@ -27,7 +27,7 @@ {{.LastUser.Name}}'s Avatar {{.LastUser.Name}}
- {{.RelativeLastReplyAt}} + {{.RelativeLastReplyAt}}
diff --git a/themes/cosora/public/account.css b/themes/cosora/public/account.css index 85fd2acd..8e1c7c2b 100644 --- a/themes/cosora/public/account.css +++ b/themes/cosora/public/account.css @@ -76,4 +76,11 @@ } #dash_right .rowitem:not(:last-child) { margin-bottom: 8px; +} + +.validated_email { + color: green; +} +.invalid_email { + color: crimson; } \ No newline at end of file diff --git a/themes/cosora/public/main.css b/themes/cosora/public/main.css index a01da68e..d7d1f4db 100644 --- a/themes/cosora/public/main.css +++ b/themes/cosora/public/main.css @@ -1129,6 +1129,10 @@ textarea { content: "{{lang "topic.report_button_text" .}}"; } +.zone_view_topic .pageset { + margin-bottom: 14px; +} + #ip_search_container .rowlist .rowitem { padding-top: 16px; padding-bottom: 10px; diff --git a/themes/nox/public/acc_panel_common.css b/themes/nox/public/acc_panel_common.css index 95796397..f9f5c7c4 100644 --- a/themes/nox/public/acc_panel_common.css +++ b/themes/nox/public/acc_panel_common.css @@ -39,6 +39,10 @@ margin-bottom: 4px; } +.to_right { + margin-left: auto; +} + @media (max-width: 420px) { .colstack { display: block; diff --git a/themes/nox/public/account.css b/themes/nox/public/account.css index 86f5d154..c65664dc 100644 --- a/themes/nox/public/account.css +++ b/themes/nox/public/account.css @@ -98,3 +98,13 @@ #dash_right .rowitem:not(:last-child) { margin-bottom: 8px; } + +.rowlist .rowitem { + display: flex; +} +.validated_email { + color: rgb(0, 170, 0); +} +.invalid_email { + color: crimson; +} \ No newline at end of file diff --git a/themes/nox/public/main.css b/themes/nox/public/main.css index 9a6cb694..3c1912f5 100644 --- a/themes/nox/public/main.css +++ b/themes/nox/public/main.css @@ -788,6 +788,9 @@ input[type=checkbox]:checked + label .sel { content: "{{lang "topic.like_count_suffix" . }}"; } +.zone_view_topic .pageset { + margin-bottom: 14px; +} .topic_reply_container { display: flex; } diff --git a/themes/nox/public/panel.css b/themes/nox/public/panel.css index c815470c..bb350d6d 100644 --- a/themes/nox/public/panel.css +++ b/themes/nox/public/panel.css @@ -67,7 +67,7 @@ background-color: rgb(88,68,68); } -.to_right, .panel_buttons, .panel_floater { +.panel_buttons, .panel_floater { margin-left: auto; } diff --git a/themes/shadow/public/account.css b/themes/shadow/public/account.css index 5f9f5528..73c58952 100644 --- a/themes/shadow/public/account.css +++ b/themes/shadow/public/account.css @@ -45,4 +45,11 @@ } .rowmenu .account_soon, .rowmenu .dash_security { font-size: 11px; +} + +.validated_email { + color: rgb(0, 170, 0); +} +.invalid_email { + color: crimson; } \ No newline at end of file diff --git a/themes/tempra-simple/public/account.css b/themes/tempra-simple/public/account.css index bb2ffd29..1ce3e1e3 100644 --- a/themes/tempra-simple/public/account.css +++ b/themes/tempra-simple/public/account.css @@ -48,4 +48,11 @@ .account_soon, .dash_security { font-size: 14px; color: maroon; +} + +.validated_email { + color: green; +} +.invalid_email { + color: crimson; } \ No newline at end of file