Added support for two-factor authentication.

Added the Account Dashboard and merged a few account views into it.
BREAKING CHANGE: We now use config/config.json instead of config/config.go, be sure to setup one of these files, you can config_default.json as an example of what a config.json should look like. If you don't have an existing installation, you can just rely on the installer to do this for you.

CSS Changes (does not include Nox Theme):
Sidebar should no longer show up in the account manager in some odd situations or themes.
Made a few CSS rules more generic.
Forms have a new look in Cosora now.

Config Changes:
Removed the DefaultRoute config field.
Added the DefaultPath config field.
Added the MaxRequestSizeStr config field to make it easier for users to input custom max request sizes without having to use a calculator or figure out how many bytes there are in a megabyte.
Removed the CacheTopicUser config field.
Added the UserCache config field.
Added the TopicCache config field

Phrases:
Removed ten english phrases.
Added 21 english phrases.
Changed eleven english phrases.
Removed some duplicate indices in the english phrase pack.

Removed some old benchmark code.
Tweaked some things to make the linter happy.
Added comments for all the MemoryUserCache and MemoryTopicCache methods.
Added a comment for the null caches, consult the other caches for further information on the methods.
Added a client-side check to make sure the user doesn't upload too much data in a single post. The server already did this, but it might be a while before feedback arrives from it.
Simplified a lot of the control panel route code with the buildBasePage function.
Renamed /user/edit/critical/ to /user/edit/password/
Renamed /user/edit/critical/submit/ to /user/edit/password/submit/
Made some small improvements to SEO with a couple of meta tags.
Renamed some of the control panel templates so that they use _ instead of -.
Fixed a bug where notices were being moved to the wrong place in some areas in Cosora.
Added the writeJsonError function to help abstract writing json errors.
Moved routePanelUsers to panel.Users
Moved routePanelUsersEdit to panel.UsersEdit
Moved routePanelUsersEditSubmit to panel.UsersEditSubmit
Renamed routes.AccountEditCritical to routes.AccountEditPassword
Renamed routes.AccountEditCriticalSubmit to routes.AccountEditPasswordSubmit
Removed the routes.AccountEditAvatar and routes.AccountEditUsername routes.
Fixed a data race in MemoryTopicCache.Add which could lead to the capacity limit being bypassed.
Tweaked MemoryTopicCache.AddUnsafe under the assumption that it's not going to be safe anyway, but we might as-well try in case this call is properly synchronised.
Fixed a data race in MemoryTopicCache.Remove which could lead to the length counter being decremented twice.
Tweaked the behaviour of MemoryTopicCache.RemoveUnsafe to mirror that of Remove.
Fixed a data race in MemoryUserCache.Add which could lead to the capacity limit being bypassed.
User can no longer change their usernames to blank.

Made a lot of progress on the Nox theme.
Added modified FA5 SVGs as a dependency for Nox.
Be sure to run the patcher or update script and don't forget to create a customised config/config.json file.
This commit is contained in:
Azareal 2018-06-17 17:28:18 +10:00
parent 2d7f302768
commit f8f46b3c48
89 changed files with 2154 additions and 1264 deletions

View File

@ -7,7 +7,10 @@
package common package common
import ( import (
"crypto/sha256"
"crypto/subtle"
"database/sql" "database/sql"
"encoding/hex"
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
@ -35,6 +38,8 @@ var ErrTooFewHashParams = errors.New("You haven't provided enough hash parameter
// ErrPasswordTooLong is silly, but we don't want bcrypt to bork on us // ErrPasswordTooLong is silly, but we don't want bcrypt to bork on us
var ErrPasswordTooLong = errors.New("The password you selected is too long") 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 ErrWrongMFAToken = errors.New("That 2FA token isn't correct")
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
@ -58,13 +63,16 @@ var HashPrefixes = map[string]string{
// AuthInt is the main authentication interface. // AuthInt is the main authentication interface.
type AuthInt interface { type AuthInt interface {
Authenticate(username string, password string) (uid int, err error) Authenticate(username string, password string) (uid int, err error, requiresExtraAuth bool)
ValidateMFAToken(mfaToken string, uid int) error
Logout(w http.ResponseWriter, uid int) Logout(w http.ResponseWriter, uid int)
ForceLogout(uid int) error ForceLogout(uid int) error
SetCookies(w http.ResponseWriter, uid int, session string) SetCookies(w http.ResponseWriter, uid int, session string)
SetProvisionalCookies(w http.ResponseWriter, uid int, session string, signedSession string) // To avoid logging someone in until they've passed the MFA check
GetCookies(r *http.Request) (uid int, session string, err error) GetCookies(r *http.Request) (uid int, session string, err error)
SessionCheck(w http.ResponseWriter, r *http.Request) (user *User, halt bool) SessionCheck(w http.ResponseWriter, r *http.Request) (user *User, halt bool)
CreateSession(uid int) (session string, err error) CreateSession(uid int) (session string, err error)
CreateProvisionalSession(uid int) (provSession string, signedSession string, err error) // To avoid logging someone in until they've passed the MFA check
} }
// DefaultAuth is the default authenticator used by Gosora, may be swapped with an alternate authenticator in some situations. E.g. To support LDAP. // DefaultAuth is the default authenticator used by Gosora, may be swapped with an alternate authenticator in some situations. E.g. To support LDAP.
@ -85,26 +93,64 @@ func NewDefaultAuth() (*DefaultAuth, error) {
} }
// Authenticate checks if a specific username and password is valid and returns the UID for the corresponding user, if so. Otherwise, a user safe error. // Authenticate checks if a specific username and password is valid and returns the UID for the corresponding user, if so. Otherwise, a user safe error.
// IF MFA is enabled, then pass it back a flag telling the caller that authentication isn't complete yet
// TODO: Find a better way of handling errors we don't want to reach the user // TODO: Find a better way of handling errors we don't want to reach the user
func (auth *DefaultAuth) Authenticate(username string, password string) (uid int, err error) { func (auth *DefaultAuth) Authenticate(username string, password string) (uid int, err error, requiresExtraAuth bool) {
var realPassword, salt string var realPassword, salt string
err = auth.login.QueryRow(username).Scan(&uid, &realPassword, &salt) err = auth.login.QueryRow(username).Scan(&uid, &realPassword, &salt)
if err == ErrNoRows { if err == ErrNoRows {
return 0, ErrNoUserByName return 0, ErrNoUserByName, false
} else if err != nil { } else if err != nil {
LogError(err) LogError(err)
return 0, ErrSecretError return 0, ErrSecretError, false
} }
err = CheckPassword(realPassword, password, salt) err = CheckPassword(realPassword, password, salt)
if err == ErrMismatchedHashAndPassword { if err == ErrMismatchedHashAndPassword {
return 0, ErrWrongPassword return 0, ErrWrongPassword, false
} else if err != nil { } else if err != nil {
LogError(err) LogError(err)
return 0, ErrSecretError return 0, ErrSecretError, false
} }
return uid, nil _, err = MFAstore.Get(uid)
if err != sql.ErrNoRows && err != nil {
LogError(err)
return 0, ErrSecretError, false
}
if err != ErrNoRows {
return uid, nil, true
}
return uid, nil, false
}
func (auth *DefaultAuth) ValidateMFAToken(mfaToken string, uid int) error {
mfaItem, err := MFAstore.Get(uid)
if err != sql.ErrNoRows && err != nil {
LogError(err)
return ErrSecretError
}
if err != ErrNoRows {
ok, err := VerifyGAuthToken(mfaItem.Secret, mfaToken)
if err != nil {
return ErrBadMFAToken
}
if ok {
return nil
}
for i, scratch := range mfaItem.Scratch {
if subtle.ConstantTimeCompare([]byte(scratch), []byte(mfaToken)) == 1 {
err = mfaItem.BurnScratch(i)
if err != nil {
LogError(err)
return ErrSecretError
}
return nil
}
}
}
return ErrWrongMFAToken
} }
// ForceLogout logs the user out of every computer, not just the one they logged out of // ForceLogout logs the user out of every computer, not just the one they logged out of
@ -141,6 +187,17 @@ func (auth *DefaultAuth) SetCookies(w http.ResponseWriter, uid int, session stri
http.SetCookie(w, &cookie) http.SetCookie(w, &cookie)
} }
// TODO: Set the cookie domain
// SetProvisionalCookies sets the two cookies required for guests to be recognised as having passed the initial login but not having passed the additional checks (e.g. multi-factor authentication)
func (auth *DefaultAuth) SetProvisionalCookies(w http.ResponseWriter, uid int, provSession string, signedSession string) {
cookie := http.Cookie{Name: "uid", Value: strconv.Itoa(uid), Path: "/", MaxAge: int(Year)}
http.SetCookie(w, &cookie)
cookie = http.Cookie{Name: "provSession", Value: provSession, Path: "/", MaxAge: int(Year)}
http.SetCookie(w, &cookie)
cookie = http.Cookie{Name: "signedSession", Value: signedSession, Path: "/", MaxAge: int(Year)}
http.SetCookie(w, &cookie)
}
// GetCookies fetches the current user's session cookies // GetCookies fetches the current user's session cookies
func (auth *DefaultAuth) GetCookies(r *http.Request) (uid int, session string, err error) { func (auth *DefaultAuth) GetCookies(r *http.Request) (uid int, session string, err error) {
// Are there any session cookies..? // Are there any session cookies..?
@ -202,6 +259,19 @@ func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) {
return session, nil return session, nil
} }
func (auth *DefaultAuth) CreateProvisionalSession(uid int) (provSession string, signedSession string, err error) {
provSession, err = GenerateSafeString(SessionLength)
if err != nil {
return "", "", err
}
h := sha256.New()
h.Write([]byte(SessionSigningKeyBox.Load().(string)))
h.Write([]byte(provSession))
h.Write([]byte(strconv.Itoa(uid)))
return provSession, hex.EncodeToString(h.Sum(nil)), nil
}
func CheckPassword(realPassword string, password string, salt string) (err error) { func CheckPassword(realPassword string, password string, salt string) (err error) {
blasted := strings.Split(realPassword, "$") blasted := strings.Split(realPassword, "$")
prefix := blasted[0] prefix := blasted[0]
@ -274,11 +344,20 @@ func Argon2GeneratePassword(password string) (hash string, salt string, err erro
} }
*/ */
// TODO: Not sure if these work, test them with Google Authenticator // TODO: Test this with Google Authenticator proper
func FriendlyGAuthSecret(secret string) (out string) {
for i, char := range secret {
out += string(char)
if (i+1)%4 == 0 {
out += " "
}
}
return strings.TrimSpace(out)
}
func GenerateGAuthSecret() (string, error) { func GenerateGAuthSecret() (string, error) {
return GenerateSafeString(24) return GenerateStd32SafeString(14)
} }
func VerifyGAuthToken(secret string, token string) (bool, error) { func VerifyGAuthToken(secret string, token string) (bool, error) {
trueToken, err := gauth.GetTOTPToken(secret) trueToken, err := gauth.GetTOTPToken(secret)
return trueToken == token, err return subtle.ConstantTimeCompare([]byte(trueToken), []byte(token)) == 1, err
} }

View File

@ -2,12 +2,6 @@ package common
import "errors" import "errors"
// Go away, linter. We need to differentiate constants from variables somehow ;)
// nolint
const CACHE_STATIC int = 0
const CACHE_DYNAMIC int = 1
const CACHE_SQL int = 2
// nolint // nolint
// ErrCacheDesync is thrown whenever a piece of data, for instance, a user is out of sync with the database. Currently unused. // ErrCacheDesync is thrown whenever a piece of data, for instance, a user is out of sync with the database. Currently unused.
var ErrCacheDesync = errors.New("The cache is out of sync with the database.") // TODO: A cross-server synchronisation mechanism var ErrCacheDesync = errors.New("The cache is out of sync with the database.") // TODO: A cross-server synchronisation mechanism

View File

@ -25,7 +25,9 @@ var StartTime time.Time
var TmplPtrMap = make(map[string]interface{}) var TmplPtrMap = make(map[string]interface{})
// Anti-spam token with rotated key // Anti-spam token with rotated key
var JSTokenBox atomic.Value // TODO: Move this and some of these other globals somewhere else var JSTokenBox atomic.Value // TODO: Move this and some of these other globals somewhere else
var SessionSigningKeyBox atomic.Value // For MFA to avoid hitting the database unneccesarily
var OldSessionSigningKeyBox atomic.Value // Just in case we've signed with a key that's about to go stale so we don't annoy the user too much
// ErrNoRows is an alias of sql.ErrNoRows, just in case we end up with non-database/sql datastores // ErrNoRows is an alias of sql.ErrNoRows, just in case we end up with non-database/sql datastores
var ErrNoRows = sql.ErrNoRows var ErrNoRows = sql.ErrNoRows
@ -60,6 +62,8 @@ var ExecutableFileExts = StringList{
func init() { func init() {
JSTokenBox.Store("") JSTokenBox.Store("")
SessionSigningKeyBox.Store("")
OldSessionSigningKeyBox.Store("")
} }
// TODO: Write a test for this // TODO: Write a test for this

View File

@ -4,6 +4,7 @@ import (
"log" "log"
"net/http" "net/http"
"runtime/debug" "runtime/debug"
"strings"
"sync" "sync"
) )
@ -114,7 +115,7 @@ func InternalErrorJSQ(err error, w http.ResponseWriter, r *http.Request, isJs bo
// ? - Add a user parameter? // ? - Add a user parameter?
func InternalErrorJS(err error, w http.ResponseWriter, r *http.Request) RouteError { func InternalErrorJS(err error, w http.ResponseWriter, r *http.Request) RouteError {
w.WriteHeader(500) w.WriteHeader(500)
_, _ = w.Write([]byte(`{"errmsg":"A problem has occurred in the system."}`)) writeJsonError("A problem has occurred in the system.", w)
LogError(err) LogError(err)
return HandledRouteError() return HandledRouteError()
} }
@ -148,7 +149,7 @@ func PreError(errmsg string, w http.ResponseWriter, r *http.Request) RouteError
func PreErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteError { func PreErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteError {
w.WriteHeader(500) w.WriteHeader(500)
_, _ = w.Write([]byte(`{"errmsg":"` + errmsg + `"}`)) writeJsonError(errmsg, w)
return HandledRouteError() return HandledRouteError()
} }
@ -177,7 +178,7 @@ func LocalErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, user U
func LocalErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteError { func LocalErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteError {
w.WriteHeader(500) w.WriteHeader(500)
_, _ = w.Write([]byte(`{"errmsg": "` + errmsg + `"}`)) writeJsonError(errmsg, w)
return HandledRouteError() return HandledRouteError()
} }
@ -199,7 +200,7 @@ func NoPermissionsJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bo
func NoPermissionsJS(w http.ResponseWriter, r *http.Request, user User) RouteError { func NoPermissionsJS(w http.ResponseWriter, r *http.Request, user User) RouteError {
w.WriteHeader(403) w.WriteHeader(403)
_, _ = w.Write([]byte(`{"errmsg":"You don't have permission to do that."}`)) writeJsonError("You don't have permission to do that.", w)
return HandledRouteError() return HandledRouteError()
} }
@ -222,7 +223,7 @@ func BannedJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) Rou
func BannedJS(w http.ResponseWriter, r *http.Request, user User) RouteError { func BannedJS(w http.ResponseWriter, r *http.Request, user User) RouteError {
w.WriteHeader(403) w.WriteHeader(403)
_, _ = w.Write([]byte(`{"errmsg":"You have been banned from this site."}`)) writeJsonError("You have been banned from this site.", w)
return HandledRouteError() return HandledRouteError()
} }
@ -246,7 +247,7 @@ func LoginRequired(w http.ResponseWriter, r *http.Request, user User) RouteError
// nolint // nolint
func LoginRequiredJS(w http.ResponseWriter, r *http.Request, user User) RouteError { func LoginRequiredJS(w http.ResponseWriter, r *http.Request, user User) RouteError {
w.WriteHeader(401) w.WriteHeader(401)
_, _ = w.Write([]byte(`{"errmsg":"You need to login to do that."}`)) writeJsonError("You need to login to do that.", w)
return HandledRouteError() return HandledRouteError()
} }
@ -297,10 +298,15 @@ func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.Response
// CustomErrorJS is the pure JSON version of CustomError // CustomErrorJS is the pure JSON version of CustomError
func CustomErrorJS(errmsg string, errcode int, w http.ResponseWriter, r *http.Request, user User) RouteError { func CustomErrorJS(errmsg string, errcode int, w http.ResponseWriter, r *http.Request, user User) RouteError {
w.WriteHeader(errcode) w.WriteHeader(errcode)
_, _ = w.Write([]byte(`{"errmsg":"` + errmsg + `"}`)) writeJsonError(errmsg, w)
return HandledRouteError() return HandledRouteError()
} }
// TODO: Should we optimise this by caching these json strings?
func writeJsonError(errmsg string, w http.ResponseWriter) {
_, _ = w.Write([]byte(`{"errmsg":"` + strings.Replace(errmsg, "\"", "", -1) + `"}`))
}
func handleErrorTemplate(w http.ResponseWriter, r *http.Request, pi ErrorPage) { func handleErrorTemplate(w http.ResponseWriter, r *http.Request, pi ErrorPage) {
// TODO: What to do about this hook? // TODO: What to do about this hook?
if RunPreRenderHook("pre_render_error", w, r, &pi.Header.CurrentUser, &pi) { if RunPreRenderHook("pre_render_error", w, r, &pi.Header.CurrentUser, &pi) {

View File

@ -93,14 +93,16 @@ var PreRenderHooks = map[string][]func(http.ResponseWriter, *http.Request, *User
"pre_render_overview": nil, "pre_render_overview": nil,
"pre_render_create_topic": nil, "pre_render_create_topic": nil,
"pre_render_account_own_edit_password": nil, "pre_render_account_own_edit": nil,
"pre_render_account_own_edit_avatar": nil, "pre_render_account_own_edit_password": nil,
"pre_render_account_own_edit_username": nil, "pre_render_account_own_edit_mfa": nil,
"pre_render_account_own_edit_email": nil, "pre_render_account_own_edit_mfa_setup": nil,
"pre_render_login": nil, "pre_render_account_own_edit_email": nil,
"pre_render_register": nil, "pre_render_login": nil,
"pre_render_ban": nil, "pre_render_login_mfa_verify": nil,
"pre_render_ip_search": nil, "pre_render_register": nil,
"pre_render_ban": nil,
"pre_render_ip_search": nil,
"pre_render_panel_dashboard": nil, "pre_render_panel_dashboard": nil,
"pre_render_panel_forums": nil, "pre_render_panel_forums": nil,

View File

@ -121,7 +121,7 @@ func (fps *MemoryForumPermsStore) Reload(fid int) error {
group.CanSee = []int{} group.CanSee = []int{}
for _, fid := range fids { for _, fid := range fids {
DebugDetailf("Forum #%+v\n", fid) DebugDetailf("Forum #%+v\n", fid)
var forumPerms = make(map[int]*ForumPerms) var forumPerms map[int]*ForumPerms
var ok bool var ok bool
if fid%2 == 0 { if fid%2 == 0 {
fps.evenLock.RLock() fps.evenLock.RLock()

View File

@ -26,6 +26,8 @@ func prefix0(otp string) string {
} }
func GetHOTPToken(secret string, interval int64) (string, error) { func GetHOTPToken(secret string, interval int64) (string, error) {
secret = strings.Replace(secret, " ", "", -1)
// Converts secret to base32 Encoding. Base32 encoding desires a 32-character subset of the twenty-six letters AZ and ten digits 09 // Converts secret to base32 Encoding. Base32 encoding desires a 32-character subset of the twenty-six letters AZ and ten digits 09
key, err := base32.StdEncoding.DecodeString(strings.ToUpper(secret)) key, err := base32.StdEncoding.DecodeString(strings.ToUpper(secret))
if err != nil { if err != nil {

101
common/mfa_store.go Normal file
View File

@ -0,0 +1,101 @@
package common
import (
"database/sql"
"errors"
"strings"
"../query_gen/lib"
)
var MFAstore MFAStore
var ErrMFAScratchIndexOutOfBounds = errors.New("That MFA scratch index is out of bounds")
type MFAItemStmts struct {
update *sql.Stmt
delete *sql.Stmt
}
var mfaItemStmts MFAItemStmts
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
mfaItemStmts = MFAItemStmts{
update: acc.Update("users_2fa_keys").Set("scratch1 = ?, scratch2, scratch3 = ?, scratch3 = ?, scratch4 = ?, scratch5 = ?, scratch6 = ?, scratch7 = ?, scratch8 = ?").Where("uid = ?").Prepare(),
delete: acc.Delete("users_2fa_keys").Where("uid = ?").Prepare(),
}
return acc.FirstError()
})
}
type MFAItem struct {
UID int
Secret string
Scratch []string
}
func (item *MFAItem) BurnScratch(index int) error {
if index < 0 || len(item.Scratch) <= index {
return ErrMFAScratchIndexOutOfBounds
}
newScratch, err := mfaCreateScratch()
if err != nil {
return err
}
item.Scratch[index] = newScratch
_, err = mfaItemStmts.update.Exec(item.Scratch[0], item.Scratch[1], item.Scratch[2], item.Scratch[3], item.Scratch[4], item.Scratch[5], item.Scratch[6], item.Scratch[7], item.UID)
return err
}
func (item *MFAItem) Delete() error {
_, err := mfaItemStmts.delete.Exec(item.UID)
return err
}
func mfaCreateScratch() (string, error) {
code, err := GenerateStd32SafeString(8)
return strings.Replace(code, "=", "", -1), err
}
type MFAStore interface {
Get(id int) (*MFAItem, error)
Create(secret string, uid int) (err error)
}
type SQLMFAStore struct {
get *sql.Stmt
create *sql.Stmt
}
func NewSQLMFAStore(acc *qgen.Accumulator) (*SQLMFAStore, error) {
return &SQLMFAStore{
get: acc.Select("users_2fa_keys").Columns("secret, scratch1, scratch2, scratch3, scratch4, scratch5, scratch6, scratch7, scratch8").Where("uid = ?").Prepare(),
create: acc.Insert("users_2fa_keys").Columns("uid, secret, scratch1, scratch2, scratch3, scratch4, scratch5, scratch6, scratch7, scratch8, createdAt").Fields("?,?,?,?,?,?,?,?,?,?,UTC_TIMESTAMP()").Prepare(),
}, acc.FirstError()
}
// TODO: Write a test for this
func (store *SQLMFAStore) Get(id int) (*MFAItem, error) {
item := MFAItem{UID: id, Scratch: make([]string, 8)}
err := store.get.QueryRow(id).Scan(&item.Secret, &item.Scratch[0], &item.Scratch[1], &item.Scratch[2], &item.Scratch[3], &item.Scratch[4], &item.Scratch[5], &item.Scratch[6], &item.Scratch[7])
return &item, err
}
// TODO: Write a test for this
func (store *SQLMFAStore) Create(secret string, uid int) (err error) {
var params = make([]interface{}, 10)
params[0] = uid
params[1] = secret
for i := 2; i < len(params); i++ {
code, err := mfaCreateScratch()
if err != nil {
return err
}
params[i] = code
}
_, err = store.create.Exec(params...)
return err
}

View File

@ -1,5 +1,6 @@
package common package common
// NullTopicCache is a topic cache to be used when you don't want a cache and just want queries to passthrough to the database
type NullTopicCache struct { type NullTopicCache struct {
} }
@ -8,48 +9,35 @@ func NewNullTopicCache() *NullTopicCache {
return &NullTopicCache{} return &NullTopicCache{}
} }
// nolint
func (mts *NullTopicCache) Get(id int) (*Topic, error) { func (mts *NullTopicCache) Get(id int) (*Topic, error) {
return nil, ErrNoRows return nil, ErrNoRows
} }
func (mts *NullTopicCache) GetUnsafe(id int) (*Topic, error) { func (mts *NullTopicCache) GetUnsafe(id int) (*Topic, error) {
return nil, ErrNoRows return nil, ErrNoRows
} }
func (mts *NullTopicCache) Set(_ *Topic) error { func (mts *NullTopicCache) Set(_ *Topic) error {
return nil return nil
} }
func (mts *NullTopicCache) Add(_ *Topic) error {
func (mts *NullTopicCache) Add(item *Topic) error {
_ = item
return nil return nil
} }
func (mts *NullTopicCache) AddUnsafe(_ *Topic) error {
// TODO: Make these length increments thread-safe. Ditto for the other DataStores
func (mts *NullTopicCache) AddUnsafe(item *Topic) error {
_ = item
return nil return nil
} }
// TODO: Make these length decrements thread-safe. Ditto for the other DataStores
func (mts *NullTopicCache) Remove(id int) error { func (mts *NullTopicCache) Remove(id int) error {
return nil return nil
} }
func (mts *NullTopicCache) RemoveUnsafe(id int) error { func (mts *NullTopicCache) RemoveUnsafe(id int) error {
return nil return nil
} }
func (mts *NullTopicCache) Flush() { func (mts *NullTopicCache) Flush() {
} }
func (mts *NullTopicCache) Length() int { func (mts *NullTopicCache) Length() int {
return 0 return 0
} }
func (mts *NullTopicCache) SetCapacity(_ int) { func (mts *NullTopicCache) SetCapacity(_ int) {
} }
func (mts *NullTopicCache) GetCapacity() int { func (mts *NullTopicCache) GetCapacity() int {
return 0 return 0
} }

View File

@ -1,5 +1,6 @@
package common package common
// NullUserCache is a user cache to be used when you don't want a cache and just want queries to passthrough to the database
type NullUserCache struct { type NullUserCache struct {
} }
@ -8,50 +9,38 @@ func NewNullUserCache() *NullUserCache {
return &NullUserCache{} return &NullUserCache{}
} }
// nolint
func (mus *NullUserCache) Get(id int) (*User, error) { func (mus *NullUserCache) Get(id int) (*User, error) {
return nil, ErrNoRows return nil, ErrNoRows
} }
func (mus *NullUserCache) BulkGet(_ []int) (list []*User) { func (mus *NullUserCache) BulkGet(_ []int) (list []*User) {
return nil return nil
} }
func (mus *NullUserCache) GetUnsafe(id int) (*User, error) { func (mus *NullUserCache) GetUnsafe(id int) (*User, error) {
return nil, ErrNoRows return nil, ErrNoRows
} }
func (mus *NullUserCache) Set(_ *User) error { func (mus *NullUserCache) Set(_ *User) error {
return nil return nil
} }
func (mus *NullUserCache) Add(_ *User) error {
func (mus *NullUserCache) Add(item *User) error {
_ = item
return nil return nil
} }
func (mus *NullUserCache) AddUnsafe(_ *User) error {
func (mus *NullUserCache) AddUnsafe(item *User) error {
_ = item
return nil return nil
} }
func (mus *NullUserCache) Remove(id int) error { func (mus *NullUserCache) Remove(id int) error {
return nil return nil
} }
func (mus *NullUserCache) RemoveUnsafe(id int) error { func (mus *NullUserCache) RemoveUnsafe(id int) error {
return nil return nil
} }
func (mus *NullUserCache) Flush() { func (mus *NullUserCache) Flush() {
} }
func (mus *NullUserCache) Length() int { func (mus *NullUserCache) Length() int {
return 0 return 0
} }
func (mus *NullUserCache) SetCapacity(_ int) { func (mus *NullUserCache) SetCapacity(_ int) {
} }
func (mus *NullUserCache) GetCapacity() int { func (mus *NullUserCache) GetCapacity() int {
return 0 return 0
} }

View File

@ -142,6 +142,11 @@ type EmailListPage struct {
Something interface{} Something interface{}
} }
type AccountDashPage struct {
*Header
MFASetup bool
}
type PanelStats struct { type PanelStats struct {
Users int Users int
Groups int Groups int

View File

@ -15,12 +15,12 @@ var InvalidTopic = []byte("<span style='color: red;'>[Invalid Topic]</span>")
var InvalidProfile = []byte("<span style='color: red;'>[Invalid Profile]</span>") var InvalidProfile = []byte("<span style='color: red;'>[Invalid Profile]</span>")
var InvalidForum = []byte("<span style='color: red;'>[Invalid Forum]</span>") var InvalidForum = []byte("<span style='color: red;'>[Invalid Forum]</span>")
var unknownMedia = []byte("<span style='color: red;'>[Unknown Media]</span>") var unknownMedia = []byte("<span style='color: red;'>[Unknown Media]</span>")
var UrlOpen = []byte("<a href='") var URLOpen = []byte("<a href='")
var UrlOpen2 = []byte("'>") var URLOpen2 = []byte("'>")
var bytesSinglequote = []byte("'") var bytesSinglequote = []byte("'")
var bytesGreaterthan = []byte(">") var bytesGreaterthan = []byte(">")
var urlMention = []byte(" class='mention'") var urlMention = []byte(" class='mention'")
var UrlClose = []byte("</a>") var URLClose = []byte("</a>")
var imageOpen = []byte("<a href=\"") var imageOpen = []byte("<a href=\"")
var imageOpen2 = []byte("\"><img src='") var imageOpen2 = []byte("\"><img src='")
var imageClose = []byte("' class='postImage' /></a>") var imageClose = []byte("' class='postImage' /></a>")
@ -319,13 +319,13 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
continue continue
} }
outbytes = append(outbytes, UrlOpen...) outbytes = append(outbytes, URLOpen...)
var urlBit = []byte(BuildTopicURL("", tid)) var urlBit = []byte(BuildTopicURL("", tid))
outbytes = append(outbytes, urlBit...) outbytes = append(outbytes, urlBit...)
outbytes = append(outbytes, UrlOpen2...) outbytes = append(outbytes, URLOpen2...)
var tidBit = []byte("#tid-" + strconv.Itoa(tid)) var tidBit = []byte("#tid-" + strconv.Itoa(tid))
outbytes = append(outbytes, tidBit...) outbytes = append(outbytes, tidBit...)
outbytes = append(outbytes, UrlClose...) outbytes = append(outbytes, URLClose...)
lastItem = i lastItem = i
} else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) { } else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) {
outbytes = append(outbytes, msgbytes[lastItem:i]...) outbytes = append(outbytes, msgbytes[lastItem:i]...)
@ -341,13 +341,13 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
continue continue
} }
outbytes = append(outbytes, UrlOpen...) outbytes = append(outbytes, URLOpen...)
var urlBit = []byte(BuildTopicURL("", topic.ID)) var urlBit = []byte(BuildTopicURL("", topic.ID))
outbytes = append(outbytes, urlBit...) outbytes = append(outbytes, urlBit...)
outbytes = append(outbytes, UrlOpen2...) outbytes = append(outbytes, URLOpen2...)
var ridBit = []byte("#rid-" + strconv.Itoa(rid)) var ridBit = []byte("#rid-" + strconv.Itoa(rid))
outbytes = append(outbytes, ridBit...) outbytes = append(outbytes, ridBit...)
outbytes = append(outbytes, UrlClose...) outbytes = append(outbytes, URLClose...)
lastItem = i lastItem = i
} else if bytes.Equal(msgbytes[i+1:i+5], []byte("fid-")) { } else if bytes.Equal(msgbytes[i+1:i+5], []byte("fid-")) {
outbytes = append(outbytes, msgbytes[lastItem:i]...) outbytes = append(outbytes, msgbytes[lastItem:i]...)
@ -362,13 +362,13 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
continue continue
} }
outbytes = append(outbytes, UrlOpen...) outbytes = append(outbytes, URLOpen...)
var urlBit = []byte(BuildForumURL("", fid)) var urlBit = []byte(BuildForumURL("", fid))
outbytes = append(outbytes, urlBit...) outbytes = append(outbytes, urlBit...)
outbytes = append(outbytes, UrlOpen2...) outbytes = append(outbytes, URLOpen2...)
var fidBit = []byte("#fid-" + strconv.Itoa(fid)) var fidBit = []byte("#fid-" + strconv.Itoa(fid))
outbytes = append(outbytes, fidBit...) outbytes = append(outbytes, fidBit...)
outbytes = append(outbytes, UrlClose...) outbytes = append(outbytes, URLClose...)
lastItem = i lastItem = i
} else { } else {
// TODO: Forum Shortcode Link // TODO: Forum Shortcode Link
@ -387,7 +387,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
continue continue
} }
outbytes = append(outbytes, UrlOpen...) outbytes = append(outbytes, URLOpen...)
var urlBit = []byte(menUser.Link) var urlBit = []byte(menUser.Link)
outbytes = append(outbytes, urlBit...) outbytes = append(outbytes, urlBit...)
outbytes = append(outbytes, bytesSinglequote...) outbytes = append(outbytes, bytesSinglequote...)
@ -395,7 +395,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
outbytes = append(outbytes, bytesGreaterthan...) outbytes = append(outbytes, bytesGreaterthan...)
var uidBit = []byte("@" + menUser.Name) var uidBit = []byte("@" + menUser.Name)
outbytes = append(outbytes, uidBit...) outbytes = append(outbytes, uidBit...)
outbytes = append(outbytes, UrlClose...) outbytes = append(outbytes, URLClose...)
lastItem = i lastItem = i
} else if msgbytes[i] == 'h' || msgbytes[i] == 'f' || msgbytes[i] == 'g' || msgbytes[i] == '/' { } else if msgbytes[i] == 'h' || msgbytes[i] == 'f' || msgbytes[i] == 'g' || msgbytes[i] == '/' {
if msgbytes[i+1] == 't' && msgbytes[i+2] == 't' && msgbytes[i+3] == 'p' { if msgbytes[i+1] == 't' && msgbytes[i+2] == 't' && msgbytes[i+3] == 'p' {
@ -463,11 +463,11 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
continue continue
} }
outbytes = append(outbytes, UrlOpen...) outbytes = append(outbytes, URLOpen...)
outbytes = append(outbytes, msgbytes[i:i+urlLen]...) outbytes = append(outbytes, msgbytes[i:i+urlLen]...)
outbytes = append(outbytes, UrlOpen2...) outbytes = append(outbytes, URLOpen2...)
outbytes = append(outbytes, msgbytes[i:i+urlLen]...) outbytes = append(outbytes, msgbytes[i:i+urlLen]...)
outbytes = append(outbytes, UrlClose...) outbytes = append(outbytes, URLClose...)
i += urlLen i += urlLen
lastItem = i lastItem = i
} }

View File

@ -1,7 +1,11 @@
package common package common
import ( import (
"encoding/json"
"errors" "errors"
"io/ioutil"
"log"
"strconv"
"strings" "strings"
) )
@ -9,13 +13,13 @@ import (
var Site = &site{Name: "Magical Fairy Land", Language: "english"} var Site = &site{Name: "Magical Fairy Land", Language: "english"}
// DbConfig holds the database configuration // DbConfig holds the database configuration
var DbConfig = dbConfig{Host: "localhost"} var DbConfig = &dbConfig{Host: "localhost"}
// Config holds the more technical settings // Config holds the more technical settings
var Config config var Config = new(config)
// Dev holds build flags and other things which should only be modified during developers or to gather additional test data // Dev holds build flags and other things which should only be modified during developers or to gather additional test data
var Dev devConfig var Dev = new(devConfig)
type site struct { type site struct {
ShortName string ShortName string
@ -28,6 +32,8 @@ type site struct {
EnableEmails bool EnableEmails bool
HasProxy bool HasProxy bool
Language string Language string
MaxRequestSize int // Alias, do not modify, will be overwritten
} }
type dbConfig struct { type dbConfig struct {
@ -53,9 +59,11 @@ type config struct {
SslFullchain string SslFullchain string
HashAlgo string // Defaults to bcrypt, and in the future, possibly something stronger HashAlgo string // Defaults to bcrypt, and in the future, possibly something stronger
MaxRequestSizeStr string
MaxRequestSize int MaxRequestSize int
CacheTopicUser int UserCache string
UserCacheCapacity int UserCacheCapacity int
TopicCache string
TopicCacheCapacity int TopicCacheCapacity int
SMTPServer string SMTPServer string
@ -64,9 +72,9 @@ type config struct {
SMTPPort string SMTPPort string
//SMTPEnableTLS bool //SMTPEnableTLS bool
DefaultRoute string DefaultPath string
DefaultGroup int DefaultGroup int // Should be a setting in the database
ActivationGroup int ActivationGroup int // Should be a setting in the database
StaffCSS string // ? - Move this into the settings table? Might be better to implement this as Group CSS StaffCSS string // ? - Move this into the settings table? Might be better to implement this as Group CSS
DefaultForum int // The forum posts go in by default, this used to be covered by the Uncategorised Forum, but we want to replace it with a more robust solution. Make this a setting? DefaultForum int // The forum posts go in by default, this used to be covered by the Uncategorised Forum, but we want to replace it with a more robust solution. Make this a setting?
MinifyTemplates bool MinifyTemplates bool
@ -87,7 +95,35 @@ type devConfig struct {
TestDB bool TestDB bool
} }
func ProcessConfig() error { // configHolder is purely for having a big struct to unmarshal data into
type configHolder struct {
Site *site
Config *config
Database *dbConfig
Dev *devConfig
}
func LoadConfig() error {
data, err := ioutil.ReadFile("./config/config.json")
if err != nil {
return err
}
var config configHolder
err = json.Unmarshal(data, &config)
if err != nil {
return err
}
Site = config.Site
Config = config.Config
DbConfig = config.Database
Dev = config.Dev
return nil
}
func ProcessConfig() (err error) {
Config.Noavatar = strings.Replace(Config.Noavatar, "{site_url}", Site.URL, -1) Config.Noavatar = strings.Replace(Config.Noavatar, "{site_url}", Site.URL, -1)
Site.Host = Site.URL Site.Host = Site.URL
if Site.Port != "80" && Site.Port != "443" { if Site.Port != "80" && Site.Port != "443" {
@ -96,6 +132,37 @@ func ProcessConfig() error {
Site.URL = strings.TrimSuffix(Site.URL, ":") Site.URL = strings.TrimSuffix(Site.URL, ":")
Site.URL = Site.URL + ":" + Site.Port Site.URL = Site.URL + ":" + Site.Port
} }
if Config.DefaultPath == "" {
Config.DefaultPath = "/topics/"
}
// TODO: Bump the size of max request size up, if it's too low
Config.MaxRequestSize, err = strconv.Atoi(Config.MaxRequestSizeStr)
if err != nil {
reqSizeStr := Config.MaxRequestSizeStr
if len(reqSizeStr) < 3 {
return errors.New("Invalid unit for MaxRequestSizeStr")
}
quantity, err := strconv.Atoi(reqSizeStr[:len(reqSizeStr)-2])
if err != nil {
return errors.New("Unable to convert quantity to integer in MaxRequestSizeStr, found " + reqSizeStr[:len(reqSizeStr)-2])
}
unit := reqSizeStr[len(reqSizeStr)-2:]
// TODO: Make it a named error just in case new errors are added in here in the future
Config.MaxRequestSize, err = FriendlyUnitToBytes(quantity, unit)
if err != nil {
return errors.New("Unable to recognise unit for MaxRequestSizeStr, found " + unit)
}
}
if Dev.DebugMode {
log.Print("Set MaxRequestSize to ", Config.MaxRequestSize)
}
if Config.MaxRequestSize <= 0 {
log.Fatal("MaxRequestSize should not be zero or below")
}
Site.MaxRequestSize = Config.MaxRequestSize
// ? Find a way of making these unlimited if zero? It might rule out some optimisations, waste memory, and break layouts // ? Find a way of making these unlimited if zero? It might rule out some optimisations, waste memory, and break layouts
if Config.MaxTopicTitleLength == 0 { if Config.MaxTopicTitleLength == 0 {

View File

@ -434,7 +434,8 @@ func InitTemplates() error {
if !ok { if !ok {
panic("phraseNameInt is not a string") panic("phraseNameInt is not a string")
} }
return GetTmplPhrase(phraseName) // TODO: Log non-existent phrases? // TODO: Log non-existent phrases?
return GetTmplPhrase(phraseName)
} }
fmap["scope"] = func(name interface{}) interface{} { fmap["scope"] = func(name interface{}) interface{} {

View File

@ -212,6 +212,15 @@ func (theme *Theme) MapTemplates() {
default: default:
LogError(errors.New("The source and destination templates are incompatible")) LogError(errors.New("The source and destination templates are incompatible"))
} }
case *func(AccountDashPage, io.Writer) error:
switch sTmplPtr := sourceTmplPtr.(type) {
case *func(AccountDashPage, io.Writer) error:
//overridenTemplates[themeTmpl.Name] = d_tmpl_ptr
overridenTemplates[themeTmpl.Name] = true
*dTmplPtr = *sTmplPtr
default:
LogError(errors.New("The source and destination templates are incompatible"))
}
case *func(ErrorPage, io.Writer) error: case *func(ErrorPage, io.Writer) error:
switch sTmplPtr := sourceTmplPtr.(type) { switch sTmplPtr := sourceTmplPtr.(type) {
case *func(ErrorPage, io.Writer) error: case *func(ErrorPage, io.Writer) error:

View File

@ -250,6 +250,13 @@ func ResetTemplateOverrides() {
default: default:
LogError(errors.New("The source and destination templates are incompatible")) LogError(errors.New("The source and destination templates are incompatible"))
} }
case func(AccountDashPage, io.Writer) error:
switch dPtr := destTmplPtr.(type) {
case *func(AccountDashPage, io.Writer) error:
*dPtr = oPtr
default:
LogError(errors.New("The source and destination templates are incompatible"))
}
case func(ErrorPage, io.Writer) error: case func(ErrorPage, io.Writer) error:
switch dPtr := destTmplPtr.(type) { switch dPtr := destTmplPtr.(type) {
case *func(ErrorPage, io.Writer) error: case *func(ErrorPage, io.Writer) error:
@ -304,6 +311,9 @@ func RunThemeTemplate(theme string, template string, pi interface{}, w io.Writer
case *func(IPSearchPage, io.Writer) error: case *func(IPSearchPage, io.Writer) error:
var tmpl = *tmplO var tmpl = *tmplO
return tmpl(pi.(IPSearchPage), w) return tmpl(pi.(IPSearchPage), w)
case *func(AccountDashPage, io.Writer) error:
var tmpl = *tmplO
return tmpl(pi.(AccountDashPage), w)
case *func(ErrorPage, io.Writer) error: case *func(ErrorPage, io.Writer) error:
var tmpl = *tmplO var tmpl = *tmplO
return tmpl(pi.(ErrorPage), w) return tmpl(pi.(ErrorPage), w)
@ -326,6 +336,8 @@ func RunThemeTemplate(theme string, template string, pi interface{}, w io.Writer
return tmplO(pi.(CreateTopicPage), w) return tmplO(pi.(CreateTopicPage), w)
case func(IPSearchPage, io.Writer) error: case func(IPSearchPage, io.Writer) error:
return tmplO(pi.(IPSearchPage), w) return tmplO(pi.(IPSearchPage), w)
case func(AccountDashPage, io.Writer) error:
return tmplO(pi.(AccountDashPage), w)
case func(ErrorPage, io.Writer) error: case func(ErrorPage, io.Writer) error:
return tmplO(pi.(ErrorPage), w) return tmplO(pi.(ErrorPage), w)
case func(Page, io.Writer) error: case func(Page, io.Writer) error:

View File

@ -5,6 +5,7 @@ import (
"sync/atomic" "sync/atomic"
) )
// TopicCache is an interface which spits out topics from a fast cache rather than the database, whether from memory or from an application like Redis. Topics may not be present in the cache but may be in the database
type TopicCache interface { type TopicCache interface {
Get(id int) (*Topic, error) Get(id int) (*Topic, error)
GetUnsafe(id int) (*Topic, error) GetUnsafe(id int) (*Topic, error)
@ -19,6 +20,7 @@ type TopicCache interface {
GetCapacity() int GetCapacity() int
} }
// MemoryTopicCache stores and pulls topics out of the current process' memory
type MemoryTopicCache struct { type MemoryTopicCache struct {
items map[int]*Topic items map[int]*Topic
length int64 // sync/atomic only lets us operate on int32s and int64s length int64 // sync/atomic only lets us operate on int32s and int64s
@ -35,6 +37,7 @@ func NewMemoryTopicCache(capacity int) *MemoryTopicCache {
} }
} }
// Get fetches a topic by ID. Returns ErrNoRows if not present.
func (mts *MemoryTopicCache) Get(id int) (*Topic, error) { func (mts *MemoryTopicCache) Get(id int) (*Topic, error) {
mts.RLock() mts.RLock()
item, ok := mts.items[id] item, ok := mts.items[id]
@ -45,6 +48,7 @@ func (mts *MemoryTopicCache) Get(id int) (*Topic, error) {
return item, ErrNoRows return item, ErrNoRows
} }
// GetUnsafe fetches a topic by ID. Returns ErrNoRows if not present. THIS METHOD IS NOT THREAD-SAFE.
func (mts *MemoryTopicCache) GetUnsafe(id int) (*Topic, error) { func (mts *MemoryTopicCache) GetUnsafe(id int) (*Topic, error) {
item, ok := mts.items[id] item, ok := mts.items[id]
if ok { if ok {
@ -53,6 +57,7 @@ func (mts *MemoryTopicCache) GetUnsafe(id int) (*Topic, error) {
return item, ErrNoRows return item, ErrNoRows
} }
// Set overwrites the value of a topic in the cache, whether it's present or not. May return a capacity overflow error.
func (mts *MemoryTopicCache) Set(item *Topic) error { func (mts *MemoryTopicCache) Set(item *Topic) error {
mts.Lock() mts.Lock()
_, ok := mts.items[item.ID] _, ok := mts.items[item.ID]
@ -69,42 +74,56 @@ func (mts *MemoryTopicCache) Set(item *Topic) error {
return nil return nil
} }
// Add adds a topic to the cache, similar to Set, but it's only intended for new items. This method might be deprecated in the near future, use Set. May return a capacity overflow error.
// ? Is this redundant if we have Set? Are the efficiency wins worth this? Is this even used?
func (mts *MemoryTopicCache) Add(item *Topic) error { func (mts *MemoryTopicCache) Add(item *Topic) error {
mts.Lock()
if int(mts.length) >= mts.capacity { if int(mts.length) >= mts.capacity {
mts.Unlock()
return ErrStoreCapacityOverflow return ErrStoreCapacityOverflow
} }
mts.Lock()
mts.items[item.ID] = item mts.items[item.ID] = item
mts.Unlock() mts.Unlock()
atomic.AddInt64(&mts.length, 1) atomic.AddInt64(&mts.length, 1)
return nil return nil
} }
// TODO: Make these length increments thread-safe. Ditto for the other DataStores // AddUnsafe is the unsafe version of Add. May return a capacity overflow error. THIS METHOD IS NOT THREAD-SAFE.
func (mts *MemoryTopicCache) AddUnsafe(item *Topic) error { func (mts *MemoryTopicCache) AddUnsafe(item *Topic) error {
if int(mts.length) >= mts.capacity { if int(mts.length) >= mts.capacity {
return ErrStoreCapacityOverflow return ErrStoreCapacityOverflow
} }
mts.items[item.ID] = item mts.items[item.ID] = item
atomic.AddInt64(&mts.length, 1) mts.length = int64(len(mts.items))
return nil return nil
} }
// TODO: Make these length decrements thread-safe. Ditto for the other DataStores // Remove removes a topic from the cache by ID, if they exist. Returns ErrNoRows if no items exist.
func (mts *MemoryTopicCache) Remove(id int) error { func (mts *MemoryTopicCache) Remove(id int) error {
mts.Lock() mts.Lock()
_, ok := mts.items[id]
if !ok {
mts.Unlock()
return ErrNoRows
}
delete(mts.items, id) delete(mts.items, id)
mts.Unlock() mts.Unlock()
atomic.AddInt64(&mts.length, -1) atomic.AddInt64(&mts.length, -1)
return nil return nil
} }
// RemoveUnsafe is the unsafe version of Remove. THIS METHOD IS NOT THREAD-SAFE.
func (mts *MemoryTopicCache) RemoveUnsafe(id int) error { func (mts *MemoryTopicCache) RemoveUnsafe(id int) error {
_, ok := mts.items[id]
if !ok {
return ErrNoRows
}
delete(mts.items, id) delete(mts.items, id)
atomic.AddInt64(&mts.length, -1) atomic.AddInt64(&mts.length, -1)
return nil return nil
} }
// Flush removes all the topics from the cache, useful for tests.
func (mts *MemoryTopicCache) Flush() { func (mts *MemoryTopicCache) Flush() {
mts.Lock() mts.Lock()
mts.items = make(map[int]*Topic) mts.items = make(map[int]*Topic)
@ -118,10 +137,13 @@ func (mts *MemoryTopicCache) Length() int {
return int(mts.length) return int(mts.length)
} }
// SetCapacity sets the maximum number of topics which this cache can hold
func (mts *MemoryTopicCache) SetCapacity(capacity int) { func (mts *MemoryTopicCache) SetCapacity(capacity int) {
// Ints are moved in a single instruction, so this should be thread-safe
mts.capacity = capacity mts.capacity = capacity
} }
// GetCapacity returns the maximum number of topics this cache can hold
func (mts *MemoryTopicCache) GetCapacity() int { func (mts *MemoryTopicCache) GetCapacity() int {
return mts.capacity return mts.capacity
} }

View File

@ -61,6 +61,7 @@ type UserStmts struct {
setUsername *sql.Stmt setUsername *sql.Stmt
incrementTopics *sql.Stmt incrementTopics *sql.Stmt
updateLevel *sql.Stmt updateLevel *sql.Stmt
update *sql.Stmt
// TODO: Split these into a sub-struct // TODO: Split these into a sub-struct
incrementScore *sql.Stmt incrementScore *sql.Stmt
@ -81,13 +82,15 @@ func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
var where = "uid = ?" var where = "uid = ?"
userStmts = UserStmts{ userStmts = UserStmts{
activate: acc.SimpleUpdate("users", "active = 1", where), activate: acc.SimpleUpdate("users", "active = 1", where),
changeGroup: acc.SimpleUpdate("users", "group = ?", where), // TODO: Implement user_count for users_groups here changeGroup: acc.SimpleUpdate("users", "group = ?", where), // TODO: Implement user_count for users_groups here
delete: acc.SimpleDelete("users", where), delete: acc.SimpleDelete("users", where),
setAvatar: acc.Update("users").Set("avatar = ?").Where(where).Prepare(), setAvatar: acc.Update("users").Set("avatar = ?").Where(where).Prepare(),
setUsername: acc.Update("users").Set("name = ?").Where(where).Prepare(), setUsername: acc.Update("users").Set("name = ?").Where(where).Prepare(),
incrementTopics: acc.SimpleUpdate("users", "topics = topics + ?", where), incrementTopics: acc.SimpleUpdate("users", "topics = topics + ?", where),
updateLevel: acc.SimpleUpdate("users", "level = ?", where), updateLevel: acc.SimpleUpdate("users", "level = ?", where),
update: acc.Update("users").Set("name = ?, email = ?, group = ?").Where("uid = ?").Prepare(), // TODO: Implement user_count for users_groups on things which use this
incrementScore: acc.SimpleUpdate("users", "score = score + ?", where), incrementScore: acc.SimpleUpdate("users", "score = score + ?", where),
incrementPosts: acc.SimpleUpdate("users", "posts = posts + ?", where), incrementPosts: acc.SimpleUpdate("users", "posts = posts + ?", where),
incrementBigposts: acc.SimpleUpdate("users", "posts = posts + ?, bigposts = bigposts + ?", where), incrementBigposts: acc.SimpleUpdate("users", "posts = posts + ?, bigposts = bigposts + ?", where),
@ -253,6 +256,10 @@ func (user *User) UpdateIP(host string) error {
return err return err
} }
func (user *User) Update(newname string, newemail string, newgroup int) (err error) {
return user.bindStmt(userStmts.update, newname, newemail, newgroup)
}
func (user *User) IncreasePostStats(wcount int, topic bool) (err error) { func (user *User) IncreasePostStats(wcount int, topic bool) (err error) {
var mod int var mod int
baseScore := 1 baseScore := 1

View File

@ -5,6 +5,7 @@ import (
"sync/atomic" "sync/atomic"
) )
// UserCache is an interface which spits out users from a fast cache rather than the database, whether from memory or from an application like Redis. Users may not be present in the cache but may be in the database
type UserCache interface { type UserCache interface {
Get(id int) (*User, error) Get(id int) (*User, error)
GetUnsafe(id int) (*User, error) GetUnsafe(id int) (*User, error)
@ -20,6 +21,7 @@ type UserCache interface {
GetCapacity() int GetCapacity() int
} }
// MemoryUserCache stores and pulls users out of the current process' memory
type MemoryUserCache struct { type MemoryUserCache struct {
items map[int]*User items map[int]*User
length int64 length int64
@ -36,6 +38,7 @@ func NewMemoryUserCache(capacity int) *MemoryUserCache {
} }
} }
// Get fetches a user by ID. Returns ErrNoRows if not present.
func (mus *MemoryUserCache) Get(id int) (*User, error) { func (mus *MemoryUserCache) Get(id int) (*User, error) {
mus.RLock() mus.RLock()
item, ok := mus.items[id] item, ok := mus.items[id]
@ -46,6 +49,7 @@ func (mus *MemoryUserCache) Get(id int) (*User, error) {
return item, ErrNoRows return item, ErrNoRows
} }
// BulkGet fetches multiple users by their IDs. Indices without users will be set to nil, so make sure you check for those, we might want to change this behaviour to make it less confusing.
func (mus *MemoryUserCache) BulkGet(ids []int) (list []*User) { func (mus *MemoryUserCache) BulkGet(ids []int) (list []*User) {
list = make([]*User, len(ids)) list = make([]*User, len(ids))
mus.RLock() mus.RLock()
@ -56,6 +60,7 @@ func (mus *MemoryUserCache) BulkGet(ids []int) (list []*User) {
return list return list
} }
// GetUnsafe fetches a user by ID. Returns ErrNoRows if not present. THIS METHOD IS NOT THREAD-SAFE.
func (mus *MemoryUserCache) GetUnsafe(id int) (*User, error) { func (mus *MemoryUserCache) GetUnsafe(id int) (*User, error) {
item, ok := mus.items[id] item, ok := mus.items[id]
if ok { if ok {
@ -64,6 +69,7 @@ func (mus *MemoryUserCache) GetUnsafe(id int) (*User, error) {
return item, ErrNoRows return item, ErrNoRows
} }
// Set overwrites the value of a user in the cache, whether it's present or not. May return a capacity overflow error.
func (mus *MemoryUserCache) Set(item *User) error { func (mus *MemoryUserCache) Set(item *User) error {
mus.Lock() mus.Lock()
user, ok := mus.items[item.ID] user, ok := mus.items[item.ID]
@ -81,17 +87,21 @@ func (mus *MemoryUserCache) Set(item *User) error {
return nil return nil
} }
// Add adds a user to the cache, similar to Set, but it's only intended for new items. This method might be deprecated in the near future, use Set. May return a capacity overflow error.
// ? Is this redundant if we have Set? Are the efficiency wins worth this? Is this even used?
func (mus *MemoryUserCache) Add(item *User) error { func (mus *MemoryUserCache) Add(item *User) error {
mus.Lock()
if int(mus.length) >= mus.capacity { if int(mus.length) >= mus.capacity {
mus.Unlock()
return ErrStoreCapacityOverflow return ErrStoreCapacityOverflow
} }
mus.Lock()
mus.items[item.ID] = item mus.items[item.ID] = item
mus.length = int64(len(mus.items)) mus.length = int64(len(mus.items))
mus.Unlock() mus.Unlock()
return nil return nil
} }
// AddUnsafe is the unsafe version of Add. May return a capacity overflow error. THIS METHOD IS NOT THREAD-SAFE.
func (mus *MemoryUserCache) AddUnsafe(item *User) error { func (mus *MemoryUserCache) AddUnsafe(item *User) error {
if int(mus.length) >= mus.capacity { if int(mus.length) >= mus.capacity {
return ErrStoreCapacityOverflow return ErrStoreCapacityOverflow
@ -101,6 +111,7 @@ func (mus *MemoryUserCache) AddUnsafe(item *User) error {
return nil return nil
} }
// Remove removes a user from the cache by ID, if they exist. Returns ErrNoRows if no items exist.
func (mus *MemoryUserCache) Remove(id int) error { func (mus *MemoryUserCache) Remove(id int) error {
mus.Lock() mus.Lock()
_, ok := mus.items[id] _, ok := mus.items[id]
@ -114,6 +125,7 @@ func (mus *MemoryUserCache) Remove(id int) error {
return nil return nil
} }
// RemoveUnsafe is the unsafe version of Remove. THIS METHOD IS NOT THREAD-SAFE.
func (mus *MemoryUserCache) RemoveUnsafe(id int) error { func (mus *MemoryUserCache) RemoveUnsafe(id int) error {
_, ok := mus.items[id] _, ok := mus.items[id]
if !ok { if !ok {
@ -124,6 +136,7 @@ func (mus *MemoryUserCache) RemoveUnsafe(id int) error {
return nil return nil
} }
// Flush removes all the users from the cache, useful for tests.
func (mus *MemoryUserCache) Flush() { func (mus *MemoryUserCache) Flush() {
mus.Lock() mus.Lock()
mus.items = make(map[int]*User) mus.items = make(map[int]*User)
@ -137,10 +150,13 @@ func (mus *MemoryUserCache) Length() int {
return int(mus.length) return int(mus.length)
} }
// SetCapacity sets the maximum number of users which this cache can hold
func (mus *MemoryUserCache) SetCapacity(capacity int) { func (mus *MemoryUserCache) SetCapacity(capacity int) {
// Ints are moved in a single instruction, so this should be thread-safe
mus.capacity = capacity mus.capacity = capacity
} }
// GetCapacity returns the maximum number of users this cache can hold
func (mus *MemoryUserCache) GetCapacity() int { func (mus *MemoryUserCache) GetCapacity() int {
return mus.capacity return mus.capacity
} }

View File

@ -8,6 +8,7 @@ package common
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base32"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@ -41,7 +42,7 @@ func (version *Version) String() (out string) {
return return
} }
// GenerateSafeString is for generating a cryptographically secure set of random bytes... // GenerateSafeString is for generating a cryptographically secure set of random bytes which is base64 encoded and safe for URLs
// TODO: Write a test for this // TODO: Write a test for this
func GenerateSafeString(length int) (string, error) { func GenerateSafeString(length int) (string, error) {
rb := make([]byte, length) rb := make([]byte, length)
@ -52,6 +53,17 @@ func GenerateSafeString(length int) (string, error) {
return base64.URLEncoding.EncodeToString(rb), nil return base64.URLEncoding.EncodeToString(rb), nil
} }
// GenerateStd32SafeString is for generating a cryptographically secure set of random bytes which is base32 encoded
// ? - Safe for URLs? Mostly likely due to the small range of characters
func GenerateStd32SafeString(length int) (string, error) {
rb := make([]byte, length)
_, err := rand.Read(rb)
if err != nil {
return "", err
}
return base32.StdEncoding.EncodeToString(rb), nil
}
// TODO: Write a test for this // TODO: Write a test for this
func RelativeTimeFromString(in string) (string, error) { func RelativeTimeFromString(in string) (string, error) {
if in == "" { if in == "" {
@ -147,6 +159,27 @@ func ConvertByteInUnit(bytes float64, unit string) (count float64) {
return return
} }
// TODO: Write a test for this
func FriendlyUnitToBytes(quantity int, unit string) (bytes int, err error) {
switch unit {
case "PB":
bytes = quantity * Petabyte
case "TB":
bytes = quantity * Terabyte
case "GB":
bytes = quantity * Gigabyte
case "MB":
bytes = quantity * Megabyte
case "KB":
bytes = quantity * Kilobyte
case "":
// Do nothing
default:
return bytes, errors.New("Unknown unit")
}
return bytes, nil
}
// TODO: Write a test for this // TODO: Write a test for this
// TODO: Re-add T as int64 // TODO: Re-add T as int64
func ConvertUnit(num int) (int, string) { func ConvertUnit(num int) (int, string) {

View File

@ -0,0 +1,55 @@
{
"Site": {
"ShortName":"Exa",
"Name":"Example",
"URL":"localhost",
"Port":"80",
"EnableSsl":false,
"EnableEmails":false,
"HasProxy":false,
"Language": "english"
},
"Config": {
"SslPrivkey": "",
"SslFullchain": "",
"SMTPServer": "",
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPPort": "25",
"MaxRequestSizeStr":"5MB",
"UserCache":"static",
"TopicCache":"static",
"UserCacheCapacity":120,
"TopicCacheCapacity":200,
"DefaultPath":"/topics/",
"DefaultGroup":3,
"ActivationGroup":5,
"StaffCSS":"staff_post",
"DefaultForum":2,
"MinifyTemplates":true,
"BuildSlugs":true,
"ServerCount":1,
"Noavatar":"https://api.adorable.io/avatars/285/{id}@{site_url}.png",
"ItemsPerPage":25
},
"Database": {
"Adapter": "mysql",
"Host": "localhost",
"Username": "anything_but_root",
"Password": "please_use_a_password_that_is_actually_secure",
"Dbname": "gosora",
"Port": "3306",
"TestAdapter": "mysql",
"TestHost": "localhost",
"TestUsername": "root",
"TestPassword": "password",
"TestDbname": "gosora_test",
"TestPort": "3306"
},
"Dev": {
"DebugMode":true,
"SuperDebug":false
}
}

View File

@ -1,66 +0,0 @@
package config
import "../common"
func Config() {
// Site Info
common.Site.ShortName = "Ts" // This should be less than three letters to fit in the navbar
common.Site.Name = "Test Site"
common.Site.Email = ""
common.Site.URL = "localhost"
common.Site.Port = "8080" // 8080
common.Site.EnableSsl = false
common.Site.EnableEmails = false
common.Site.HasProxy = false // Cloudflare counts as this, if it's sitting in the middle
common.Config.SslPrivkey = ""
common.Config.SslFullchain = ""
common.Site.Language = "english"
// Database details
common.DbConfig.Host = "localhost"
common.DbConfig.Username = "root"
common.DbConfig.Password = "password"
common.DbConfig.Dbname = "gosora"
common.DbConfig.Port = "3306" // You probably won't need to change this
// MySQL Test Database details
common.DbConfig.TestHost = "localhost"
common.DbConfig.TestUsername = "root"
common.DbConfig.TestPassword = ""
common.DbConfig.TestDbname = "gosora_test" // The name of the test database, leave blank to disable. DON'T USE YOUR PRODUCTION DATABASE FOR THIS. LEAVE BLANK IF YOU DON'T KNOW WHAT THIS MEANS.
common.DbConfig.TestPort = "3306"
// Limiters
common.Config.MaxRequestSize = 5 * common.Megabyte
// Caching
common.Config.CacheTopicUser = common.CACHE_STATIC
common.Config.UserCacheCapacity = 120 // The max number of users held in memory
common.Config.TopicCacheCapacity = 200 // The max number of topics held in memory
// Email
common.Config.SMTPServer = ""
common.Config.SMTPUsername = ""
common.Config.SMTPPassword = ""
common.Config.SMTPPort = "25"
// Misc
common.Config.DefaultRoute = "routes.TopicList"
common.Config.DefaultGroup = 3 // Should be a setting in the database
common.Config.ActivationGroup = 5 // Should be a setting in the database
common.Config.StaffCSS = "staff_post"
common.Config.DefaultForum = 2
common.Config.MinifyTemplates = true
common.Config.ServerCount = 1 // Experimental: Enable Cross-Server Synchronisation and several other features
//common.Config.Noavatar = "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png"
common.Config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png"
common.Config.ItemsPerPage = 25
// Developer flags
//common.Dev.DebugMode = true
//common.Dev.SuperDebug = true
//common.Dev.TemplateDebug = true
//common.Dev.Profiling = true
//common.Dev.TestDB = true
}

View File

@ -47,9 +47,12 @@ func InitDatabase() (err error) {
log.Print("Initialising the user and topic stores") log.Print("Initialising the user and topic stores")
var ucache common.UserCache var ucache common.UserCache
var tcache common.TopicCache if common.Config.UserCache == "static" {
if common.Config.CacheTopicUser == common.CACHE_STATIC {
ucache = common.NewMemoryUserCache(common.Config.UserCacheCapacity) ucache = common.NewMemoryUserCache(common.Config.UserCacheCapacity)
}
var tcache common.TopicCache
if common.Config.TopicCache == "static" {
tcache = common.NewMemoryTopicCache(common.Config.TopicCacheCapacity) tcache = common.NewMemoryTopicCache(common.Config.TopicCacheCapacity)
} }

3
docs/landing_page.md Normal file
View File

@ -0,0 +1,3 @@
# Landing Page
You can change the landing page of your site (in other words, the page the user lands on by default, aka the index or `/`) by tweaking the DefaultPath configuration value. More on this later.

View File

@ -21,7 +21,6 @@ type Stmts struct {
updatePlugin *sql.Stmt updatePlugin *sql.Stmt
updatePluginInstall *sql.Stmt updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt updateTheme *sql.Stmt
updateUser *sql.Stmt
updateGroupPerms *sql.Stmt updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt updateGroup *sql.Stmt
updateEmail *sql.Stmt updateEmail *sql.Stmt
@ -141,14 +140,6 @@ func _gen_mssql() (err error) {
return err return err
} }
common.DebugLog("Preparing updateUser statement.")
stmts.updateUser, err = db.Prepare("UPDATE [users] SET [name] = ?,[email] = ?,[group] = ? WHERE [uid] = ?")
if err != nil {
log.Print("Error in updateUser statement.")
log.Print("Bad Query: ","UPDATE [users] SET [name] = ?,[email] = ?,[group] = ? WHERE [uid] = ?")
return err
}
common.DebugLog("Preparing updateGroupPerms statement.") common.DebugLog("Preparing updateGroupPerms statement.")
stmts.updateGroupPerms, err = db.Prepare("UPDATE [users_groups] SET [permissions] = ? WHERE [gid] = ?") stmts.updateGroupPerms, err = db.Prepare("UPDATE [users_groups] SET [permissions] = ? WHERE [gid] = ?")
if err != nil { if err != nil {

View File

@ -23,7 +23,6 @@ type Stmts struct {
updatePlugin *sql.Stmt updatePlugin *sql.Stmt
updatePluginInstall *sql.Stmt updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt updateTheme *sql.Stmt
updateUser *sql.Stmt
updateGroupPerms *sql.Stmt updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt updateGroup *sql.Stmt
updateEmail *sql.Stmt updateEmail *sql.Stmt
@ -131,13 +130,6 @@ func _gen_mysql() (err error) {
return err return err
} }
common.DebugLog("Preparing updateUser statement.")
stmts.updateUser, err = db.Prepare("UPDATE `users` SET `name` = ?,`email` = ?,`group` = ? WHERE `uid` = ?")
if err != nil {
log.Print("Error in updateUser statement.")
return err
}
common.DebugLog("Preparing updateGroupPerms statement.") common.DebugLog("Preparing updateGroupPerms statement.")
stmts.updateGroupPerms, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?") stmts.updateGroupPerms, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?")
if err != nil { if err != nil {

View File

@ -16,7 +16,6 @@ type Stmts struct {
updatePlugin *sql.Stmt updatePlugin *sql.Stmt
updatePluginInstall *sql.Stmt updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt updateTheme *sql.Stmt
updateUser *sql.Stmt
updateGroupPerms *sql.Stmt updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt updateGroup *sql.Stmt
updateEmail *sql.Stmt updateEmail *sql.Stmt
@ -87,13 +86,6 @@ func _gen_pgsql() (err error) {
return err return err
} }
common.DebugLog("Preparing updateUser statement.")
stmts.updateUser, err = db.Prepare("UPDATE `users` SET `name` = ?,`email` = ?,`group` = ? WHERE `uid` = ?")
if err != nil {
log.Print("Error in updateUser statement.")
return err
}
common.DebugLog("Preparing updateGroupPerms statement.") common.DebugLog("Preparing updateGroupPerms statement.")
stmts.updateGroupPerms, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?") stmts.updateGroupPerms, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?")
if err != nil { if err != nil {

View File

@ -66,9 +66,9 @@ var RouteMap = map[string]interface{}{
"routePanelPluginsActivate": routePanelPluginsActivate, "routePanelPluginsActivate": routePanelPluginsActivate,
"routePanelPluginsDeactivate": routePanelPluginsDeactivate, "routePanelPluginsDeactivate": routePanelPluginsDeactivate,
"routePanelPluginsInstall": routePanelPluginsInstall, "routePanelPluginsInstall": routePanelPluginsInstall,
"routePanelUsers": routePanelUsers, "panel.Users": panel.Users,
"routePanelUsersEdit": routePanelUsersEdit, "panel.UsersEdit": panel.UsersEdit,
"routePanelUsersEditSubmit": routePanelUsersEditSubmit, "panel.UsersEditSubmit": panel.UsersEditSubmit,
"panel.AnalyticsViews": panel.AnalyticsViews, "panel.AnalyticsViews": panel.AnalyticsViews,
"panel.AnalyticsRoutes": panel.AnalyticsRoutes, "panel.AnalyticsRoutes": panel.AnalyticsRoutes,
"panel.AnalyticsAgents": panel.AnalyticsAgents, "panel.AnalyticsAgents": panel.AnalyticsAgents,
@ -95,12 +95,15 @@ var RouteMap = map[string]interface{}{
"panel.LogsMod": panel.LogsMod, "panel.LogsMod": panel.LogsMod,
"panel.Debug": panel.Debug, "panel.Debug": panel.Debug,
"routePanelDashboard": routePanelDashboard, "routePanelDashboard": routePanelDashboard,
"routes.AccountEditCritical": routes.AccountEditCritical, "routes.AccountEdit": routes.AccountEdit,
"routes.AccountEditCriticalSubmit": routes.AccountEditCriticalSubmit, "routes.AccountEditPassword": routes.AccountEditPassword,
"routes.AccountEditAvatar": routes.AccountEditAvatar, "routes.AccountEditPasswordSubmit": routes.AccountEditPasswordSubmit,
"routes.AccountEditAvatarSubmit": routes.AccountEditAvatarSubmit, "routes.AccountEditAvatarSubmit": routes.AccountEditAvatarSubmit,
"routes.AccountEditUsername": routes.AccountEditUsername,
"routes.AccountEditUsernameSubmit": routes.AccountEditUsernameSubmit, "routes.AccountEditUsernameSubmit": routes.AccountEditUsernameSubmit,
"routes.AccountEditMFA": routes.AccountEditMFA,
"routes.AccountEditMFASetup": routes.AccountEditMFASetup,
"routes.AccountEditMFASetupSubmit": routes.AccountEditMFASetupSubmit,
"routes.AccountEditMFADisableSubmit": routes.AccountEditMFADisableSubmit,
"routes.AccountEditEmail": routes.AccountEditEmail, "routes.AccountEditEmail": routes.AccountEditEmail,
"routes.AccountEditEmailTokenSubmit": routes.AccountEditEmailTokenSubmit, "routes.AccountEditEmailTokenSubmit": routes.AccountEditEmailTokenSubmit,
"routes.ViewProfile": routes.ViewProfile, "routes.ViewProfile": routes.ViewProfile,
@ -131,6 +134,8 @@ var RouteMap = map[string]interface{}{
"routes.AccountRegister": routes.AccountRegister, "routes.AccountRegister": routes.AccountRegister,
"routes.AccountLogout": routes.AccountLogout, "routes.AccountLogout": routes.AccountLogout,
"routes.AccountLoginSubmit": routes.AccountLoginSubmit, "routes.AccountLoginSubmit": routes.AccountLoginSubmit,
"routes.AccountLoginMFAVerify": routes.AccountLoginMFAVerify,
"routes.AccountLoginMFAVerifySubmit": routes.AccountLoginMFAVerifySubmit,
"routes.AccountRegisterSubmit": routes.AccountRegisterSubmit, "routes.AccountRegisterSubmit": routes.AccountRegisterSubmit,
"routes.DynamicRoute": routes.DynamicRoute, "routes.DynamicRoute": routes.DynamicRoute,
"routes.UploadedFile": routes.UploadedFile, "routes.UploadedFile": routes.UploadedFile,
@ -188,9 +193,9 @@ var routeMapEnum = map[string]int{
"routePanelPluginsActivate": 43, "routePanelPluginsActivate": 43,
"routePanelPluginsDeactivate": 44, "routePanelPluginsDeactivate": 44,
"routePanelPluginsInstall": 45, "routePanelPluginsInstall": 45,
"routePanelUsers": 46, "panel.Users": 46,
"routePanelUsersEdit": 47, "panel.UsersEdit": 47,
"routePanelUsersEditSubmit": 48, "panel.UsersEditSubmit": 48,
"panel.AnalyticsViews": 49, "panel.AnalyticsViews": 49,
"panel.AnalyticsRoutes": 50, "panel.AnalyticsRoutes": 50,
"panel.AnalyticsAgents": 51, "panel.AnalyticsAgents": 51,
@ -217,49 +222,54 @@ var routeMapEnum = map[string]int{
"panel.LogsMod": 72, "panel.LogsMod": 72,
"panel.Debug": 73, "panel.Debug": 73,
"routePanelDashboard": 74, "routePanelDashboard": 74,
"routes.AccountEditCritical": 75, "routes.AccountEdit": 75,
"routes.AccountEditCriticalSubmit": 76, "routes.AccountEditPassword": 76,
"routes.AccountEditAvatar": 77, "routes.AccountEditPasswordSubmit": 77,
"routes.AccountEditAvatarSubmit": 78, "routes.AccountEditAvatarSubmit": 78,
"routes.AccountEditUsername": 79, "routes.AccountEditUsernameSubmit": 79,
"routes.AccountEditUsernameSubmit": 80, "routes.AccountEditMFA": 80,
"routes.AccountEditEmail": 81, "routes.AccountEditMFASetup": 81,
"routes.AccountEditEmailTokenSubmit": 82, "routes.AccountEditMFASetupSubmit": 82,
"routes.ViewProfile": 83, "routes.AccountEditMFADisableSubmit": 83,
"routes.BanUserSubmit": 84, "routes.AccountEditEmail": 84,
"routes.UnbanUser": 85, "routes.AccountEditEmailTokenSubmit": 85,
"routes.ActivateUser": 86, "routes.ViewProfile": 86,
"routes.IPSearch": 87, "routes.BanUserSubmit": 87,
"routes.CreateTopicSubmit": 88, "routes.UnbanUser": 88,
"routes.EditTopicSubmit": 89, "routes.ActivateUser": 89,
"routes.DeleteTopicSubmit": 90, "routes.IPSearch": 90,
"routes.StickTopicSubmit": 91, "routes.CreateTopicSubmit": 91,
"routes.UnstickTopicSubmit": 92, "routes.EditTopicSubmit": 92,
"routes.LockTopicSubmit": 93, "routes.DeleteTopicSubmit": 93,
"routes.UnlockTopicSubmit": 94, "routes.StickTopicSubmit": 94,
"routes.MoveTopicSubmit": 95, "routes.UnstickTopicSubmit": 95,
"routes.LikeTopicSubmit": 96, "routes.LockTopicSubmit": 96,
"routes.ViewTopic": 97, "routes.UnlockTopicSubmit": 97,
"routes.CreateReplySubmit": 98, "routes.MoveTopicSubmit": 98,
"routes.ReplyEditSubmit": 99, "routes.LikeTopicSubmit": 99,
"routes.ReplyDeleteSubmit": 100, "routes.ViewTopic": 100,
"routes.ReplyLikeSubmit": 101, "routes.CreateReplySubmit": 101,
"routes.ProfileReplyCreateSubmit": 102, "routes.ReplyEditSubmit": 102,
"routes.ProfileReplyEditSubmit": 103, "routes.ReplyDeleteSubmit": 103,
"routes.ProfileReplyDeleteSubmit": 104, "routes.ReplyLikeSubmit": 104,
"routes.PollVote": 105, "routes.ProfileReplyCreateSubmit": 105,
"routes.PollResults": 106, "routes.ProfileReplyEditSubmit": 106,
"routes.AccountLogin": 107, "routes.ProfileReplyDeleteSubmit": 107,
"routes.AccountRegister": 108, "routes.PollVote": 108,
"routes.AccountLogout": 109, "routes.PollResults": 109,
"routes.AccountLoginSubmit": 110, "routes.AccountLogin": 110,
"routes.AccountRegisterSubmit": 111, "routes.AccountRegister": 111,
"routes.DynamicRoute": 112, "routes.AccountLogout": 112,
"routes.UploadedFile": 113, "routes.AccountLoginSubmit": 113,
"routes.StaticFile": 114, "routes.AccountLoginMFAVerify": 114,
"routes.RobotsTxt": 115, "routes.AccountLoginMFAVerifySubmit": 115,
"routes.SitemapXml": 116, "routes.AccountRegisterSubmit": 116,
"routes.BadRoute": 117, "routes.DynamicRoute": 117,
"routes.UploadedFile": 118,
"routes.StaticFile": 119,
"routes.RobotsTxt": 120,
"routes.SitemapXml": 121,
"routes.BadRoute": 122,
} }
var reverseRouteMapEnum = map[int]string{ var reverseRouteMapEnum = map[int]string{
0: "routeAPI", 0: "routeAPI",
@ -308,9 +318,9 @@ var reverseRouteMapEnum = map[int]string{
43: "routePanelPluginsActivate", 43: "routePanelPluginsActivate",
44: "routePanelPluginsDeactivate", 44: "routePanelPluginsDeactivate",
45: "routePanelPluginsInstall", 45: "routePanelPluginsInstall",
46: "routePanelUsers", 46: "panel.Users",
47: "routePanelUsersEdit", 47: "panel.UsersEdit",
48: "routePanelUsersEditSubmit", 48: "panel.UsersEditSubmit",
49: "panel.AnalyticsViews", 49: "panel.AnalyticsViews",
50: "panel.AnalyticsRoutes", 50: "panel.AnalyticsRoutes",
51: "panel.AnalyticsAgents", 51: "panel.AnalyticsAgents",
@ -337,49 +347,54 @@ var reverseRouteMapEnum = map[int]string{
72: "panel.LogsMod", 72: "panel.LogsMod",
73: "panel.Debug", 73: "panel.Debug",
74: "routePanelDashboard", 74: "routePanelDashboard",
75: "routes.AccountEditCritical", 75: "routes.AccountEdit",
76: "routes.AccountEditCriticalSubmit", 76: "routes.AccountEditPassword",
77: "routes.AccountEditAvatar", 77: "routes.AccountEditPasswordSubmit",
78: "routes.AccountEditAvatarSubmit", 78: "routes.AccountEditAvatarSubmit",
79: "routes.AccountEditUsername", 79: "routes.AccountEditUsernameSubmit",
80: "routes.AccountEditUsernameSubmit", 80: "routes.AccountEditMFA",
81: "routes.AccountEditEmail", 81: "routes.AccountEditMFASetup",
82: "routes.AccountEditEmailTokenSubmit", 82: "routes.AccountEditMFASetupSubmit",
83: "routes.ViewProfile", 83: "routes.AccountEditMFADisableSubmit",
84: "routes.BanUserSubmit", 84: "routes.AccountEditEmail",
85: "routes.UnbanUser", 85: "routes.AccountEditEmailTokenSubmit",
86: "routes.ActivateUser", 86: "routes.ViewProfile",
87: "routes.IPSearch", 87: "routes.BanUserSubmit",
88: "routes.CreateTopicSubmit", 88: "routes.UnbanUser",
89: "routes.EditTopicSubmit", 89: "routes.ActivateUser",
90: "routes.DeleteTopicSubmit", 90: "routes.IPSearch",
91: "routes.StickTopicSubmit", 91: "routes.CreateTopicSubmit",
92: "routes.UnstickTopicSubmit", 92: "routes.EditTopicSubmit",
93: "routes.LockTopicSubmit", 93: "routes.DeleteTopicSubmit",
94: "routes.UnlockTopicSubmit", 94: "routes.StickTopicSubmit",
95: "routes.MoveTopicSubmit", 95: "routes.UnstickTopicSubmit",
96: "routes.LikeTopicSubmit", 96: "routes.LockTopicSubmit",
97: "routes.ViewTopic", 97: "routes.UnlockTopicSubmit",
98: "routes.CreateReplySubmit", 98: "routes.MoveTopicSubmit",
99: "routes.ReplyEditSubmit", 99: "routes.LikeTopicSubmit",
100: "routes.ReplyDeleteSubmit", 100: "routes.ViewTopic",
101: "routes.ReplyLikeSubmit", 101: "routes.CreateReplySubmit",
102: "routes.ProfileReplyCreateSubmit", 102: "routes.ReplyEditSubmit",
103: "routes.ProfileReplyEditSubmit", 103: "routes.ReplyDeleteSubmit",
104: "routes.ProfileReplyDeleteSubmit", 104: "routes.ReplyLikeSubmit",
105: "routes.PollVote", 105: "routes.ProfileReplyCreateSubmit",
106: "routes.PollResults", 106: "routes.ProfileReplyEditSubmit",
107: "routes.AccountLogin", 107: "routes.ProfileReplyDeleteSubmit",
108: "routes.AccountRegister", 108: "routes.PollVote",
109: "routes.AccountLogout", 109: "routes.PollResults",
110: "routes.AccountLoginSubmit", 110: "routes.AccountLogin",
111: "routes.AccountRegisterSubmit", 111: "routes.AccountRegister",
112: "routes.DynamicRoute", 112: "routes.AccountLogout",
113: "routes.UploadedFile", 113: "routes.AccountLoginSubmit",
114: "routes.StaticFile", 114: "routes.AccountLoginMFAVerify",
115: "routes.RobotsTxt", 115: "routes.AccountLoginMFAVerifySubmit",
116: "routes.SitemapXml", 116: "routes.AccountRegisterSubmit",
117: "routes.BadRoute", 117: "routes.DynamicRoute",
118: "routes.UploadedFile",
119: "routes.StaticFile",
120: "routes.RobotsTxt",
121: "routes.SitemapXml",
122: "routes.BadRoute",
} }
var osMapEnum = map[string]int{ var osMapEnum = map[string]int{
"unknown": 0, "unknown": 0,
@ -615,9 +630,9 @@ func (router *GenRouter) SuspiciousRequest(req *http.Request, prepend string) {
counters.AgentViewCounter.Bump(27) counters.AgentViewCounter.Bump(27)
} }
// TODO: Pass the default route or config struct to the router rather than accessing it via a package global // TODO: Pass the default path or config struct to the router rather than accessing it via a package global
// TODO: SetDefaultRoute // TODO: SetDefaultPath
// TODO: GetDefaultRoute // TODO: GetDefaultPath
func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Redirect www. requests to the right place // Redirect www. requests to the right place
if req.Host == "www." + common.Site.Host { if req.Host == "www." + common.Site.Host {
@ -655,6 +670,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if strings.Contains(req.URL.Path,"..") || strings.Contains(req.URL.Path,"--") || strings.Contains(lowerPath,".php") || strings.Contains(lowerPath,".asp") || strings.Contains(lowerPath,".cgi") || strings.Contains(lowerPath,".py") || strings.Contains(lowerPath,".sql") || strings.Contains(lowerPath,".action") { if strings.Contains(req.URL.Path,"..") || strings.Contains(req.URL.Path,"--") || strings.Contains(lowerPath,".php") || strings.Contains(lowerPath,".asp") || strings.Contains(lowerPath,".cgi") || strings.Contains(lowerPath,".py") || strings.Contains(lowerPath,".sql") || strings.Contains(lowerPath,".action") {
router.SuspiciousRequest(req,"") router.SuspiciousRequest(req,"")
} }
// Indirect the default route onto a different one
if req.URL.Path == "/" {
req.URL.Path = common.Config.DefaultPath
}
var prefix, extraData string var prefix, extraData string
prefix = req.URL.Path[0:strings.IndexByte(req.URL.Path[1:],'/') + 1] prefix = req.URL.Path[0:strings.IndexByte(req.URL.Path[1:],'/') + 1]
@ -670,7 +690,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
counters.GlobalViewCounter.Bump() counters.GlobalViewCounter.Bump()
if prefix == "/static" { if prefix == "/static" {
counters.RouteViewCounter.Bump(114) counters.RouteViewCounter.Bump(119)
req.URL.Path += extraData req.URL.Path += extraData
routes.StaticFile(w, req) routes.StaticFile(w, req)
return return
@ -1213,10 +1233,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
err = routePanelPluginsInstall(w,req,user,extraData) err = routePanelPluginsInstall(w,req,user,extraData)
case "/panel/users/": case "/panel/users/":
counters.RouteViewCounter.Bump(46) counters.RouteViewCounter.Bump(46)
err = routePanelUsers(w,req,user) err = panel.Users(w,req,user)
case "/panel/users/edit/": case "/panel/users/edit/":
counters.RouteViewCounter.Bump(47) counters.RouteViewCounter.Bump(47)
err = routePanelUsersEdit(w,req,user,extraData) err = panel.UsersEdit(w,req,user,extraData)
case "/panel/users/edit/submit/": case "/panel/users/edit/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
@ -1225,7 +1245,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(48) counters.RouteViewCounter.Bump(48)
err = routePanelUsersEditSubmit(w,req,user,extraData) err = panel.UsersEditSubmit(w,req,user,extraData)
case "/panel/analytics/views/": case "/panel/analytics/views/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
if err != nil { if err != nil {
@ -1394,7 +1414,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
case "/user": case "/user":
switch(req.URL.Path) { switch(req.URL.Path) {
case "/user/edit/critical/": case "/user/edit/":
err = common.MemberOnly(w,req,user) err = common.MemberOnly(w,req,user)
if err != nil { if err != nil {
router.handleError(err,w,req,user) router.handleError(err,w,req,user)
@ -1402,8 +1422,17 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(75) counters.RouteViewCounter.Bump(75)
err = routes.AccountEditCritical(w,req,user) err = routes.AccountEdit(w,req,user)
case "/user/edit/critical/submit/": case "/user/edit/password/":
err = common.MemberOnly(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
counters.RouteViewCounter.Bump(76)
err = routes.AccountEditPassword(w,req,user)
case "/user/edit/password/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
router.handleError(err,w,req,user) router.handleError(err,w,req,user)
@ -1416,17 +1445,8 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(76)
err = routes.AccountEditCriticalSubmit(w,req,user)
case "/user/edit/avatar/":
err = common.MemberOnly(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
counters.RouteViewCounter.Bump(77) counters.RouteViewCounter.Bump(77)
err = routes.AccountEditAvatar(w,req,user) err = routes.AccountEditPasswordSubmit(w,req,user)
case "/user/edit/avatar/submit/": case "/user/edit/avatar/submit/":
err = common.MemberOnly(w,req,user) err = common.MemberOnly(w,req,user)
if err != nil { if err != nil {
@ -1447,15 +1467,6 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
counters.RouteViewCounter.Bump(78) counters.RouteViewCounter.Bump(78)
err = routes.AccountEditAvatarSubmit(w,req,user) err = routes.AccountEditAvatarSubmit(w,req,user)
case "/user/edit/username/":
err = common.MemberOnly(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
counters.RouteViewCounter.Bump(79)
err = routes.AccountEditUsername(w,req,user)
case "/user/edit/username/submit/": case "/user/edit/username/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
if err != nil { if err != nil {
@ -1469,9 +1480,18 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(80) counters.RouteViewCounter.Bump(79)
err = routes.AccountEditUsernameSubmit(w,req,user) err = routes.AccountEditUsernameSubmit(w,req,user)
case "/user/edit/email/": case "/user/edit/mfa/":
err = common.MemberOnly(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
counters.RouteViewCounter.Bump(80)
err = routes.AccountEditMFA(w,req,user)
case "/user/edit/mfa/setup/":
err = common.MemberOnly(w,req,user) err = common.MemberOnly(w,req,user)
if err != nil { if err != nil {
router.handleError(err,w,req,user) router.handleError(err,w,req,user)
@ -1479,6 +1499,45 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
counters.RouteViewCounter.Bump(81) counters.RouteViewCounter.Bump(81)
err = routes.AccountEditMFASetup(w,req,user)
case "/user/edit/mfa/setup/submit/":
err = common.NoSessionMismatch(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
err = common.MemberOnly(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
counters.RouteViewCounter.Bump(82)
err = routes.AccountEditMFASetupSubmit(w,req,user)
case "/user/edit/mfa/disable/submit/":
err = common.NoSessionMismatch(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
err = common.MemberOnly(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
counters.RouteViewCounter.Bump(83)
err = routes.AccountEditMFADisableSubmit(w,req,user)
case "/user/edit/email/":
err = common.MemberOnly(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
counters.RouteViewCounter.Bump(84)
err = routes.AccountEditEmail(w,req,user) err = routes.AccountEditEmail(w,req,user)
case "/user/edit/token/": case "/user/edit/token/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1493,11 +1552,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(82) counters.RouteViewCounter.Bump(85)
err = routes.AccountEditEmailTokenSubmit(w,req,user,extraData) err = routes.AccountEditEmailTokenSubmit(w,req,user,extraData)
default: default:
req.URL.Path += extraData req.URL.Path += extraData
counters.RouteViewCounter.Bump(83) counters.RouteViewCounter.Bump(86)
err = routes.ViewProfile(w,req,user) err = routes.ViewProfile(w,req,user)
} }
if err != nil { if err != nil {
@ -1518,7 +1577,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(84) counters.RouteViewCounter.Bump(87)
err = routes.BanUserSubmit(w,req,user,extraData) err = routes.BanUserSubmit(w,req,user,extraData)
case "/users/unban/": case "/users/unban/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1533,7 +1592,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(85) counters.RouteViewCounter.Bump(88)
err = routes.UnbanUser(w,req,user,extraData) err = routes.UnbanUser(w,req,user,extraData)
case "/users/activate/": case "/users/activate/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1548,7 +1607,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(86) counters.RouteViewCounter.Bump(89)
err = routes.ActivateUser(w,req,user,extraData) err = routes.ActivateUser(w,req,user,extraData)
case "/users/ips/": case "/users/ips/":
err = common.MemberOnly(w,req,user) err = common.MemberOnly(w,req,user)
@ -1557,7 +1616,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(87) counters.RouteViewCounter.Bump(90)
err = routes.IPSearch(w,req,user) err = routes.IPSearch(w,req,user)
} }
if err != nil { if err != nil {
@ -1583,7 +1642,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(88) counters.RouteViewCounter.Bump(91)
err = routes.CreateTopicSubmit(w,req,user) err = routes.CreateTopicSubmit(w,req,user)
case "/topic/edit/submit/": case "/topic/edit/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1598,7 +1657,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(89) counters.RouteViewCounter.Bump(92)
err = routes.EditTopicSubmit(w,req,user,extraData) err = routes.EditTopicSubmit(w,req,user,extraData)
case "/topic/delete/submit/": case "/topic/delete/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1614,7 +1673,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
req.URL.Path += extraData req.URL.Path += extraData
counters.RouteViewCounter.Bump(90) counters.RouteViewCounter.Bump(93)
err = routes.DeleteTopicSubmit(w,req,user) err = routes.DeleteTopicSubmit(w,req,user)
case "/topic/stick/submit/": case "/topic/stick/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1629,7 +1688,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(91) counters.RouteViewCounter.Bump(94)
err = routes.StickTopicSubmit(w,req,user,extraData) err = routes.StickTopicSubmit(w,req,user,extraData)
case "/topic/unstick/submit/": case "/topic/unstick/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1644,7 +1703,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(92) counters.RouteViewCounter.Bump(95)
err = routes.UnstickTopicSubmit(w,req,user,extraData) err = routes.UnstickTopicSubmit(w,req,user,extraData)
case "/topic/lock/submit/": case "/topic/lock/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1660,7 +1719,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
req.URL.Path += extraData req.URL.Path += extraData
counters.RouteViewCounter.Bump(93) counters.RouteViewCounter.Bump(96)
err = routes.LockTopicSubmit(w,req,user) err = routes.LockTopicSubmit(w,req,user)
case "/topic/unlock/submit/": case "/topic/unlock/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1675,7 +1734,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(94) counters.RouteViewCounter.Bump(97)
err = routes.UnlockTopicSubmit(w,req,user,extraData) err = routes.UnlockTopicSubmit(w,req,user,extraData)
case "/topic/move/submit/": case "/topic/move/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1690,7 +1749,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(95) counters.RouteViewCounter.Bump(98)
err = routes.MoveTopicSubmit(w,req,user,extraData) err = routes.MoveTopicSubmit(w,req,user,extraData)
case "/topic/like/submit/": case "/topic/like/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1711,10 +1770,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(96) counters.RouteViewCounter.Bump(99)
err = routes.LikeTopicSubmit(w,req,user,extraData) err = routes.LikeTopicSubmit(w,req,user,extraData)
default: default:
counters.RouteViewCounter.Bump(97) counters.RouteViewCounter.Bump(100)
err = routes.ViewTopic(w,req,user, extraData) err = routes.ViewTopic(w,req,user, extraData)
} }
if err != nil { if err != nil {
@ -1740,7 +1799,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(98) counters.RouteViewCounter.Bump(101)
err = routes.CreateReplySubmit(w,req,user) err = routes.CreateReplySubmit(w,req,user)
case "/reply/edit/submit/": case "/reply/edit/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1755,7 +1814,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(99) counters.RouteViewCounter.Bump(102)
err = routes.ReplyEditSubmit(w,req,user,extraData) err = routes.ReplyEditSubmit(w,req,user,extraData)
case "/reply/delete/submit/": case "/reply/delete/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1770,7 +1829,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(100) counters.RouteViewCounter.Bump(103)
err = routes.ReplyDeleteSubmit(w,req,user,extraData) err = routes.ReplyDeleteSubmit(w,req,user,extraData)
case "/reply/like/submit/": case "/reply/like/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1791,7 +1850,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(101) counters.RouteViewCounter.Bump(104)
err = routes.ReplyLikeSubmit(w,req,user,extraData) err = routes.ReplyLikeSubmit(w,req,user,extraData)
} }
if err != nil { if err != nil {
@ -1812,7 +1871,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(102) counters.RouteViewCounter.Bump(105)
err = routes.ProfileReplyCreateSubmit(w,req,user) err = routes.ProfileReplyCreateSubmit(w,req,user)
case "/profile/reply/edit/submit/": case "/profile/reply/edit/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1827,7 +1886,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(103) counters.RouteViewCounter.Bump(106)
err = routes.ProfileReplyEditSubmit(w,req,user,extraData) err = routes.ProfileReplyEditSubmit(w,req,user,extraData)
case "/profile/reply/delete/submit/": case "/profile/reply/delete/submit/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1842,7 +1901,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(104) counters.RouteViewCounter.Bump(107)
err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData)
} }
if err != nil { if err != nil {
@ -1863,10 +1922,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(105) counters.RouteViewCounter.Bump(108)
err = routes.PollVote(w,req,user,extraData) err = routes.PollVote(w,req,user,extraData)
case "/poll/results/": case "/poll/results/":
counters.RouteViewCounter.Bump(106) counters.RouteViewCounter.Bump(109)
err = routes.PollResults(w,req,user,extraData) err = routes.PollResults(w,req,user,extraData)
} }
if err != nil { if err != nil {
@ -1875,10 +1934,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
case "/accounts": case "/accounts":
switch(req.URL.Path) { switch(req.URL.Path) {
case "/accounts/login/": case "/accounts/login/":
counters.RouteViewCounter.Bump(107) counters.RouteViewCounter.Bump(110)
err = routes.AccountLogin(w,req,user) err = routes.AccountLogin(w,req,user)
case "/accounts/create/": case "/accounts/create/":
counters.RouteViewCounter.Bump(108) counters.RouteViewCounter.Bump(111)
err = routes.AccountRegister(w,req,user) err = routes.AccountRegister(w,req,user)
case "/accounts/logout/": case "/accounts/logout/":
err = common.NoSessionMismatch(w,req,user) err = common.NoSessionMismatch(w,req,user)
@ -1893,7 +1952,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(109) counters.RouteViewCounter.Bump(112)
err = routes.AccountLogout(w,req,user) err = routes.AccountLogout(w,req,user)
case "/accounts/login/submit/": case "/accounts/login/submit/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
@ -1902,8 +1961,20 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(110) counters.RouteViewCounter.Bump(113)
err = routes.AccountLoginSubmit(w,req,user) err = routes.AccountLoginSubmit(w,req,user)
case "/accounts/mfa_verify/":
counters.RouteViewCounter.Bump(114)
err = routes.AccountLoginMFAVerify(w,req,user)
case "/accounts/mfa_verify/submit/":
err = common.ParseForm(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
counters.RouteViewCounter.Bump(115)
err = routes.AccountLoginMFAVerifySubmit(w,req,user)
case "/accounts/create/submit/": case "/accounts/create/submit/":
err = common.ParseForm(w,req,user) err = common.ParseForm(w,req,user)
if err != nil { if err != nil {
@ -1911,7 +1982,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
counters.RouteViewCounter.Bump(111) counters.RouteViewCounter.Bump(116)
err = routes.AccountRegisterSubmit(w,req,user) err = routes.AccountRegisterSubmit(w,req,user)
} }
if err != nil { if err != nil {
@ -1928,7 +1999,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
common.NotFound(w,req,nil) common.NotFound(w,req,nil)
return return
} }
counters.RouteViewCounter.Bump(113) counters.RouteViewCounter.Bump(118)
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?
router.UploadHandler(w,req) // TODO: Count these views router.UploadHandler(w,req) // TODO: Count these views
@ -1937,35 +2008,22 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 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(115) counters.RouteViewCounter.Bump(120)
err = routes.RobotsTxt(w,req) err = routes.RobotsTxt(w,req)
if err != nil { if err != nil {
router.handleError(err,w,req,user) router.handleError(err,w,req,user)
} }
return return
/*case "sitemap.xml": /*case "sitemap.xml":
counters.RouteViewCounter.Bump(116) counters.RouteViewCounter.Bump(121)
err = routes.SitemapXml(w,req) err = routes.SitemapXml(w,req)
if err != nil { if err != nil {
router.handleError(err,w,req,user) router.handleError(err,w,req,user)
} }
return*/ return*/
} }
if extraData != "" { common.NotFound(w,req,nil)
common.NotFound(w,req,nil) return
return
}
handle, ok := RouteMap[common.Config.DefaultRoute]
if !ok {
// TODO: Make this a startup error not a runtime one
router.requestLogger.Print("Unable to find the default route")
common.NotFound(w,req,nil)
return
}
counters.RouteViewCounter.Bump(routeMapEnum[common.Config.DefaultRoute])
handle.(func(http.ResponseWriter, *http.Request, common.User) common.RouteError)(w,req,user)
default: default:
// A fallback for the routes which haven't been converted to the new router yet or plugins // A fallback for the routes which haven't been converted to the new router yet or plugins
router.RLock() router.RLock()
@ -1973,7 +2031,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
router.RUnlock() router.RUnlock()
if ok { if ok {
counters.RouteViewCounter.Bump(112) // TODO: Be more specific about *which* dynamic route it is counters.RouteViewCounter.Bump(117) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData req.URL.Path += extraData
err = handle(w,req,user) err = handle(w,req,user)
if err != nil { if err != nil {
@ -1988,7 +2046,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} else { } else {
router.DumpRequest(req,"Bad Route") router.DumpRequest(req,"Bad Route")
} }
counters.RouteViewCounter.Bump(117) counters.RouteViewCounter.Bump(122)
common.NotFound(w,req,nil) common.NotFound(w,req,nil)
} }
} }

View File

@ -2,20 +2,21 @@
package main package main
var dbTablePrimaryKeys = map[string]string{ var dbTablePrimaryKeys = map[string]string{
"topics": "tid", "users_groups":"gid",
"attachments": "attachID", "attachments":"attachID",
"menus": "mid", "users_replies":"rid",
"users_groups": "gid", "menu_items":"miid",
"users_groups_scheduler": "uid", "pages":"pid",
"registration_logs": "rlid", "polls":"pollID",
"word_filters": "wfid", "activity_stream":"asid",
"menu_items": "miid", "users_groups_scheduler":"uid",
"polls": "pollID", "replies":"rid",
"users_replies": "rid", "word_filters":"wfid",
"activity_stream": "asid", "menus":"mid",
"pages": "pid", "registration_logs":"rlid",
"replies": "rid", "users":"uid",
"revisions": "reviseID", "users_2fa_keys":"uid",
"users": "uid", "forums":"fid",
"forums": "fid", "topics":"tid",
"revisions":"reviseID",
} }

View File

@ -924,333 +924,6 @@ func BenchmarkQueriesSerial(b *testing.B) {
}) })
} }
// Commented until I add logic for profiling the router generator, I'm not sure what the best way of doing that is
/*func addEmptyRoutesToMux(routes []string, serveMux *http.ServeMux) {
for _, route := range routes {
serveMux.HandleFunc(route, func(_ http.ResponseWriter,_ *http.Request){})
}
}
func BenchmarkDefaultGoRouterSerial(b *testing.B) {
w := httptest.NewRecorder()
req := httptest.NewRequest("get","/topics/",bytes.NewReader(nil))
routes := make([]string, 0)
routes = append(routes,"/test/")
serveMux := http.NewServeMux()
serveMux.HandleFunc("/test/", func(_ http.ResponseWriter,_ *http.Request){})
b.Run("one-route", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
serveMux.ServeHTTP(w,req)
}
})
routes = append(routes,"/topic/")
routes = append(routes,"/forums/")
routes = append(routes,"/forum/")
routes = append(routes,"/panel/")
serveMux = http.NewServeMux()
addEmptyRoutesToMux(routes, serveMux)
b.Run("five-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
serveMux.ServeHTTP(w,req)
}
})
serveMux = http.NewServeMux()
routes = append(routes,"/panel/plugins/")
routes = append(routes,"/panel/groups/")
routes = append(routes,"/panel/settings/")
routes = append(routes,"/panel/users/")
routes = append(routes,"/panel/forums/")
addEmptyRoutesToMux(routes, serveMux)
b.Run("ten-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
serveMux.ServeHTTP(w,req)
}
})
serveMux = http.NewServeMux()
routes = append(routes,"/panel/forums/create/submit/")
routes = append(routes,"/panel/forums/delete/")
routes = append(routes,"/users/ban/")
routes = append(routes,"/panel/users/edit/")
routes = append(routes,"/panel/forums/create/")
routes = append(routes,"/users/unban/")
routes = append(routes,"/pages/")
routes = append(routes,"/users/activate/")
routes = append(routes,"/panel/forums/edit/submit/")
routes = append(routes,"/panel/plugins/activate/")
addEmptyRoutesToMux(routes, serveMux)
b.Run("twenty-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
serveMux.ServeHTTP(w,req)
}
})
serveMux = http.NewServeMux()
routes = append(routes,"/panel/plugins/deactivate/")
routes = append(routes,"/panel/plugins/install/")
routes = append(routes,"/panel/plugins/uninstall/")
routes = append(routes,"/panel/templates/")
routes = append(routes,"/panel/templates/edit/")
routes = append(routes,"/panel/templates/create/")
routes = append(routes,"/panel/templates/delete/")
routes = append(routes,"/panel/templates/edit/submit/")
routes = append(routes,"/panel/themes/")
routes = append(routes,"/panel/themes/edit/")
addEmptyRoutesToMux(routes, serveMux)
b.Run("thirty-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
serveMux.ServeHTTP(w,req)
}
})
serveMux = http.NewServeMux()
routes = append(routes,"/panel/themes/create/")
routes = append(routes,"/panel/themes/delete/")
routes = append(routes,"/panel/themes/delete/submit/")
routes = append(routes,"/panel/templates/create/submit/")
routes = append(routes,"/panel/templates/delete/submit/")
routes = append(routes,"/panel/widgets/")
routes = append(routes,"/panel/widgets/edit/")
routes = append(routes,"/panel/widgets/activate/")
routes = append(routes,"/panel/widgets/deactivate/")
routes = append(routes,"/panel/magical/wombat/path")
addEmptyRoutesToMux(routes, serveMux)
b.Run("forty-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
serveMux.ServeHTTP(w,req)
}
})
serveMux = http.NewServeMux()
routes = append(routes,"/report/")
routes = append(routes,"/report/submit/")
routes = append(routes,"/topic/create/submit/")
routes = append(routes,"/topics/create/")
routes = append(routes,"/overview/")
routes = append(routes,"/uploads/")
routes = append(routes,"/static/")
routes = append(routes,"/reply/edit/submit/")
routes = append(routes,"/reply/delete/submit/")
routes = append(routes,"/topic/edit/submit/")
addEmptyRoutesToMux(routes, serveMux)
b.Run("fifty-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
serveMux.ServeHTTP(w,req)
}
})
serveMux = http.NewServeMux()
routes = append(routes,"/topic/delete/submit/")
routes = append(routes,"/topic/stick/submit/")
routes = append(routes,"/topic/unstick/submit/")
routes = append(routes,"/accounts/login/")
routes = append(routes,"/accounts/create/")
routes = append(routes,"/accounts/logout/")
routes = append(routes,"/accounts/login/submit/")
routes = append(routes,"/accounts/create/submit/")
routes = append(routes,"/user/edit/critical/")
routes = append(routes,"/user/edit/critical/submit/")
addEmptyRoutesToMux(routes, serveMux)
b.Run("sixty-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
serveMux.ServeHTTP(w,req)
}
})
serveMux = http.NewServeMux()
routes = append(routes,"/user/edit/avatar/")
routes = append(routes,"/user/edit/avatar/submit/")
routes = append(routes,"/user/edit/username/")
routes = append(routes,"/user/edit/username/submit/")
routes = append(routes,"/profile/reply/create/")
routes = append(routes,"/profile/reply/edit/submit/")
routes = append(routes,"/profile/reply/delete/submit/")
routes = append(routes,"/arcane/tower/")
routes = append(routes,"/magical/kingdom/")
routes = append(routes,"/insert/name/here/")
addEmptyRoutesToMux(routes, serveMux)
b.Run("seventy-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
serveMux.ServeHTTP(w,req)
}
})
}
func addEmptyRoutesToCustom(routes []string, router *Router) {
for _, route := range routes {
router.HandleFunc(route, func(_ http.ResponseWriter,_ *http.Request){})
}
}
func BenchmarkCustomRouterSerial(b *testing.B) {
w := httptest.NewRecorder()
req := httptest.NewRequest("get","/topics/",bytes.NewReader(nil))
routes := make([]string, 0)
routes = append(routes,"/test/")
router := NewRouter()
router.HandleFunc("/test/", func(_ http.ResponseWriter,_ *http.Request){})
b.Run("one-route", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
router.ServeHTTP(w,req)
}
})
routes = append(routes,"/topic/")
routes = append(routes,"/forums/")
routes = append(routes,"/forum/")
routes = append(routes,"/panel/")
router = NewRouter()
addEmptyRoutesToCustom(routes, router)
b.Run("five-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
router.ServeHTTP(w,req)
}
})
router = NewRouter()
routes = append(routes,"/panel/plugins/")
routes = append(routes,"/panel/groups/")
routes = append(routes,"/panel/settings/")
routes = append(routes,"/panel/users/")
routes = append(routes,"/panel/forums/")
addEmptyRoutesToCustom(routes, router)
b.Run("ten-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
router.ServeHTTP(w,req)
}
})
router = NewRouter()
routes = append(routes,"/panel/forums/create/submit/")
routes = append(routes,"/panel/forums/delete/")
routes = append(routes,"/users/ban/")
routes = append(routes,"/panel/users/edit/")
routes = append(routes,"/panel/forums/create/")
routes = append(routes,"/users/unban/")
routes = append(routes,"/pages/")
routes = append(routes,"/users/activate/")
routes = append(routes,"/panel/forums/edit/submit/")
routes = append(routes,"/panel/plugins/activate/")
addEmptyRoutesToCustom(routes, router)
b.Run("twenty-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
router.ServeHTTP(w,req)
}
})
router = NewRouter()
routes = append(routes,"/panel/plugins/deactivate/")
routes = append(routes,"/panel/plugins/install/")
routes = append(routes,"/panel/plugins/uninstall/")
routes = append(routes,"/panel/templates/")
routes = append(routes,"/panel/templates/edit/")
routes = append(routes,"/panel/templates/create/")
routes = append(routes,"/panel/templates/delete/")
routes = append(routes,"/panel/templates/edit/submit/")
routes = append(routes,"/panel/themes/")
routes = append(routes,"/panel/themes/edit/")
addEmptyRoutesToCustom(routes, router)
b.Run("thirty-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
router.ServeHTTP(w,req)
}
})
router = NewRouter()
routes = append(routes,"/panel/themes/create/")
routes = append(routes,"/panel/themes/delete/")
routes = append(routes,"/panel/themes/delete/submit/")
routes = append(routes,"/panel/templates/create/submit/")
routes = append(routes,"/panel/templates/delete/submit/")
routes = append(routes,"/panel/widgets/")
routes = append(routes,"/panel/widgets/edit/")
routes = append(routes,"/panel/widgets/activate/")
routes = append(routes,"/panel/widgets/deactivate/")
routes = append(routes,"/panel/magical/wombat/path")
addEmptyRoutesToCustom(routes, router)
b.Run("forty-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
router.ServeHTTP(w,req)
}
})
router = NewRouter()
routes = append(routes,"/report/")
routes = append(routes,"/report/submit/")
routes = append(routes,"/topic/create/submit/")
routes = append(routes,"/topics/create/")
routes = append(routes,"/overview/")
routes = append(routes,"/uploads/")
routes = append(routes,"/static/")
routes = append(routes,"/reply/edit/submit/")
routes = append(routes,"/reply/delete/submit/")
routes = append(routes,"/topic/edit/submit/")
addEmptyRoutesToCustom(routes, router)
b.Run("fifty-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
router.ServeHTTP(w,req)
}
})
router = NewRouter()
routes = append(routes,"/topic/delete/submit/")
routes = append(routes,"/topic/stick/submit/")
routes = append(routes,"/topic/unstick/submit/")
routes = append(routes,"/accounts/login/")
routes = append(routes,"/accounts/create/")
routes = append(routes,"/accounts/logout/")
routes = append(routes,"/accounts/login/submit/")
routes = append(routes,"/accounts/create/submit/")
routes = append(routes,"/user/edit/critical/")
routes = append(routes,"/user/edit/critical/submit/")
addEmptyRoutesToCustom(routes, router)
b.Run("sixty-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
router.ServeHTTP(w,req)
}
})
router = NewRouter()
routes = append(routes,"/user/edit/avatar/")
routes = append(routes,"/user/edit/avatar/submit/")
routes = append(routes,"/user/edit/username/")
routes = append(routes,"/user/edit/username/submit/")
routes = append(routes,"/profile/reply/create/")
routes = append(routes,"/profile/reply/edit/submit/")
routes = append(routes,"/profile/reply/delete/submit/")
routes = append(routes,"/arcane/tower/")
routes = append(routes,"/magical/kingdom/")
routes = append(routes,"/insert/name/here/")
addEmptyRoutesToCustom(routes, router)
b.Run("seventy-routes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
req = httptest.NewRequest("get",routes[rand.Intn(len(routes))],bytes.NewReader(nil))
router.ServeHTTP(w,req)
}
})
}*/
// TODO: Take the attachment system into account in these parser benches // TODO: Take the attachment system into account in these parser benches
func BenchmarkParserSerial(b *testing.B) { func BenchmarkParserSerial(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()

View File

@ -96,79 +96,65 @@ func main() {
return return
} }
configContents := []byte(`package config configContents := []byte(`{
"Site": {
"ShortName":"` + siteShortName + `",
"Name":"` + siteName + `",
"URL":"` + siteURL + `",
"Port":"` + serverPort + `",
"EnableSsl":false,
"EnableEmails":false,
"HasProxy":false,
"Language": "english"
},
"Config": {
"SslPrivkey": "",
"SslFullchain": "",
"SMTPServer": "",
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPPort": "25",
import "../common" "MaxRequestSizeStr":"5MB",
"UserCache":"static",
"TopicCache":"static",
"UserCacheCapacity":120,
"TopicCacheCapacity":200,
"DefaultPath":"/topics/",
"DefaultGroup":3,
"ActivationGroup":5,
"StaffCSS":"staff_post",
"DefaultForum":2,
"MinifyTemplates":true,
"BuildSlugs":true,
"ServerCount":1,
"Noavatar":"https://api.adorable.io/avatars/285/{id}@{site_url}.png",
"ItemsPerPage":25
},
"Database": {
"Adapter": "` + adap.Name() + `",
"Host": "` + adap.DBHost() + `",
"Username": "` + adap.DBUsername() + `",
"Password": "` + adap.DBPassword() + `",
"Dbname": "` + adap.DBName() + `",
"Port": "` + adap.DBPort() + `",
func Config() { "TestAdapter": "` + adap.Name() + `",
// Site Info "TestHost": "",
common.Site.ShortName = "` + siteShortName + `" // This should be less than three letters to fit in the navbar "TestUsername": "",
common.Site.Name = "` + siteName + `" "TestPassword": "",
common.Site.Email = "" "TestDbname": "",
common.Site.URL = "` + siteURL + `" "TestPort": ""
common.Site.Port = "` + serverPort + `" },
common.Site.EnableSsl = false "Dev": {
common.Site.EnableEmails = false "DebugMode":true,
common.Site.HasProxy = false // Cloudflare counts as this, if it's sitting in the middle "SuperDebug":false
common.Config.SslPrivkey = "" }
common.Config.SslFullchain = "" }`)
common.Site.Language = "english"
// Database details
common.DbConfig.Adapter = "` + adap.Name() + `"
common.DbConfig.Host = "` + adap.DBHost() + `"
common.DbConfig.Username = "` + adap.DBUsername() + `"
common.DbConfig.Password = "` + adap.DBPassword() + `"
common.DbConfig.Dbname = "` + adap.DBName() + `"
common.DbConfig.Port = "` + adap.DBPort() + `" // You probably won't need to change this
// Test Database details
common.DbConfig.TestAdapter = "` + adap.Name() + `"
common.DbConfig.TestHost = ""
common.DbConfig.TestUsername = ""
common.DbConfig.TestPassword = ""
common.DbConfig.TestDbname = "" // The name of the test database, leave blank to disable. DON'T USE YOUR PRODUCTION DATABASE FOR THIS. LEAVE BLANK IF YOU DON'T KNOW WHAT THIS MEANS.
common.DbConfig.TestPort = ""
// Limiters
common.Config.MaxRequestSize = 5 * common.Megabyte
// Caching
common.Config.CacheTopicUser = common.CACHE_STATIC
common.Config.UserCacheCapacity = 120 // The max number of users held in memory
common.Config.TopicCacheCapacity = 200 // The max number of topics held in memory
// Email
common.Config.SMTPServer = ""
common.Config.SMTPUsername = ""
common.Config.SMTPPassword = ""
common.Config.SMTPPort = "25"
// Misc
common.Config.DefaultRoute = "routes.TopicList"
common.Config.DefaultGroup = 3 // Should be a setting in the database
common.Config.ActivationGroup = 5 // Should be a setting in the database
common.Config.StaffCSS = "staff_post"
common.Config.DefaultForum = 2
common.Config.MinifyTemplates = true
common.Config.BuildSlugs = true
common.Config.ServerCount = 1 // Experimental: Enable Cross-Server Synchronisation and several other features
//common.Config.Noavatar = "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png"
common.Config.Noavatar = "https://api.adorable.io/avatars/285/{id}@{site_url}.png"
common.Config.ItemsPerPage = 25
// Developer flags
common.Dev.DebugMode = true
//common.Dev.SuperDebug = true
//common.Dev.TemplateDebug = true
//common.Dev.Profiling = true
//common.Dev.TestDB = true
}
`)
//"Noavatar": "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png" Maybe allow this sort of syntax?
fmt.Println("Opening the configuration file") fmt.Println("Opening the configuration file")
configFile, err := os.Create("./config/config.go") configFile, err := os.Create("./config/config.json")
if err != nil { if err != nil {
abortError(err) abortError(err)
return return

View File

@ -81,8 +81,10 @@
"login":"Login", "login":"Login",
"register":"Registration", "register":"Registration",
"ip_search":"IP Search", "ip_search":"IP Search",
"account_username":"Edit Username", "account":"My Account",
"account_avatar":"Edit Avatar", "account_password":"Edit Password",
"account_mfa":"Manage 2FA",
"account_mfa_setup":"Setup 2FA",
"account_email":"Email Manager", "account_email":"Email Manager",
"panel_dashboard":"Control Panel Dashboard", "panel_dashboard":"Control Panel Dashboard",
@ -244,19 +246,20 @@
"NoticePhrases": { "NoticePhrases": {
"account_banned":"Your account has been suspended. Some of your permissions may have been revoked.", "account_banned":"Your account has been suspended. Some of your permissions may have been revoked.",
"account_inactive":"Your account hasn't been activated yet. Some features may remain unavailable until it is.", "account_inactive":"Your account hasn't been activated yet. Some features may remain unavailable until it is.",
"account_avatar_updated":"Your avatar was successfully updated", "account_avatar_updated":"Your avatar was successfully updated.",
"account_username_updated":"Your username was successfully updated", "account_username_updated":"Your username was successfully updated.",
"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.",
"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.",
"panel_forum_updated":"The forum was successfully updated", "panel_forum_updated":"The forum was successfully updated.",
"panel_forum_perms_updated":"The forum permissions were successfully updated", "panel_forum_perms_updated":"The forum permissions were successfully updated.",
"panel_user_updated":"The user was successfully updated", "panel_user_updated":"The user was successfully updated.",
"panel_page_created":"The page was successfully created", "panel_page_created":"The page was successfully created.",
"panel_page_updated":"The page was successfully updated", "panel_page_updated":"The page was successfully updated.",
"panel_page_deleted":"The page was successfully deleted" "panel_page_deleted":"The page was successfully deleted."
}, },
"TmplPhrases": { "TmplPhrases": {
@ -303,11 +306,13 @@
"panel_rank_guests":"Guests", "panel_rank_guests":"Guests",
"panel_rank_members":"Members", "panel_rank_members":"Members",
"panel_preset_everyone":"Everyone",
"panel_preset_announcements":"Announcements", "panel_preset_announcements":"Announcements",
"panel_preset_member_only":"Member Only", "panel_preset_member_only":"Member Only",
"panel_preset_staff_only":"Staff Only", "panel_preset_staff_only":"Staff Only",
"panel_preset_admin_only":"Admin Only", "panel_preset_admin_only":"Admin Only",
"panel_preset_archive":"Archive", "panel_preset_archive":"Archive",
"panel_preset_custom":"Custom",
"panel_preset_public":"Public", "panel_preset_public":"Public",
"panel_active_hidden":"Hidden", "panel_active_hidden":"Hidden",
@ -347,6 +352,10 @@
"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_mfa_verify_head":"2FA Verify",
"login_mfa_verify_explanation":"Please input the code from the authenticator app below.",
"login_mfa_verify_button":"Confirm",
"register_head":"Create Account", "register_head":"Create Account",
"register_account_name":"Account Name", "register_account_name":"Account Name",
"register_account_email":"Email", "register_account_email":"Email",
@ -356,16 +365,19 @@
"register_submit_button":"Create Account", "register_submit_button":"Create Account",
"account_menu_head":"My Account", "account_menu_head":"My Account",
"account_menu_avatar":"Avatar",
"account_menu_username":"Username",
"account_menu_password":"Password", "account_menu_password":"Password",
"account_menu_email":"Email", "account_menu_email":"Email",
"account_menu_security":"Security", "account_menu_security":"Security",
"account_menu_notifications":"Notifications", "account_menu_notifications":"Notifications",
"account_avatar_head":"Edit Avatar", "account_coming_soon":"Coming Soon",
"account_avatar_upload_label":"Upload Avatar",
"account_avatar_update_button":"Update", "account_dash_2fa_setup":"Setup your two-factor authentication.",
"account_dash_2fa_manage":"Remove or manage your two-factor authentication.",
"account_dash_next_level":"Progress to next level.",
"account_dash_security_notice":"Security",
"account_avatar_select":"Select",
"account_avatar_update_button":"Upload",
"account_email_head":"Emails", "account_email_head":"Emails",
"account_email_primary":"Primary", "account_email_primary":"Primary",
@ -373,17 +385,23 @@
"account_email_verified":"Verified", "account_email_verified":"Verified",
"account_email_resend_email":"Resend Verification Email", "account_email_resend_email":"Resend Verification Email",
"account_username_head":"Edit Username",
"account_username_current_username":"Current Username",
"account_username_new_username":"New Username",
"account_username_update_button":"Update",
"account_password_head":"Edit Password", "account_password_head":"Edit Password",
"account_password_current_password":"Current Password", "account_password_current_password":"Current Password",
"account_password_new_password":"New Password", "account_password_new_password":"New Password",
"account_password_confirm_password":"Confirm Password", "account_password_confirm_password":"Confirm Password",
"account_password_update_button":"Update", "account_password_update_button":"Update",
"account_mfa_head":"Manage 2FA",
"account_mfa_disable_explanation":"You can disable two-factor authentication on your account and go back to logging in normal with just your password by clicking on the following button.",
"account_mfa_disable_button":"Disable 2FA",
"account_mfa_scratch_head":"One Time Codes",
"account_mfa_scratch_explanation":"You can use the following codes to login without having an authenticator app generate codes for you.\n\nEach code can only be used once, a new one will replace it when it's used. These are intended as a backup, if your app fails or device (e.g. your phone) dies, be sure to keep them somewhere safe.",
"account_mfa_setup_head":"Setup 2FA",
"account_mfa_setup_explanation":"Type this secret into your Google Authenticator and type the code it gives you below. You will have to input codes provided by it for all future logins.",
"account_mfa_setup_verify":"Verify",
"account_mfa_setup_button":"Setup",
"areyousure_head":"Are you sure?", "areyousure_head":"Are you sure?",
"areyousure_continue":"Continue", "areyousure_continue":"Continue",
@ -603,13 +621,6 @@
"panel_forums_create_description":"Where all the super secret stuff happens", "panel_forums_create_description":"Where all the super secret stuff happens",
"panel_forums_active_label":"Active", "panel_forums_active_label":"Active",
"panel_forums_preset_label":"Preset", "panel_forums_preset_label":"Preset",
"panel_preset_everyone":"Everyone",
"panel_preset_announcements":"Announcements",
"panel_preset_member_only":"Member Only",
"panel_preset_staff_only":"Staff Only",
"panel_preset_admin_only":"Admin Only",
"panel_preset_archive":"Archive",
"panel_preset_custom":"Custom",
"panel_forums_create_button":"Add Forum", "panel_forums_create_button":"Add Forum",
"panel_forum_head_suffix":" Forum", "panel_forum_head_suffix":" Forum",

21
main.go
View File

@ -21,7 +21,6 @@ import (
"./common" "./common"
"./common/counters" "./common/counters"
"./config"
"./query_gen/lib" "./query_gen/lib"
"./routes" "./routes"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
@ -106,6 +105,10 @@ func afterDBInit() (err error) {
} }
log.Print("Initialising the stores") log.Print("Initialising the stores")
common.MFAstore, err = common.NewSQLMFAStore(acc)
if err != nil {
return err
}
common.Pages, err = common.NewDefaultPageStore(acc) common.Pages, err = common.NewDefaultPageStore(acc)
if err != nil { if err != nil {
return err return err
@ -207,7 +210,6 @@ func main() {
return return
} }
}()*/ }()*/
config.Config()
// TODO: Have a file for each run with the time/date the server started as the file name? // TODO: Have a file for each run with the time/date the server started as the file name?
// TODO: Log panics with recover() // TODO: Log panics with recover()
@ -227,6 +229,11 @@ func main() {
} }
common.JSTokenBox.Store(jsToken) common.JSTokenBox.Store(jsToken)
log.Print("Loading the configuration data")
err = common.LoadConfig()
if err != nil {
log.Fatal(err)
}
log.Print("Processing configuration data") log.Print("Processing configuration data")
err = common.ProcessConfig() err = common.ProcessConfig()
if err != nil { if err != nil {
@ -344,6 +351,7 @@ func main() {
} }
} }
// TODO: Write tests for these
// Run this goroutine once every half second // Run this goroutine once every half second
halfSecondTicker := time.NewTicker(time.Second / 2) halfSecondTicker := time.NewTicker(time.Second / 2)
secondTicker := time.NewTicker(time.Second) secondTicker := time.NewTicker(time.Second)
@ -394,11 +402,20 @@ func main() {
runHook("after_fifteen_minute_tick") runHook("after_fifteen_minute_tick")
case <-hourTicker.C: case <-hourTicker.C:
runHook("before_hour_tick") runHook("before_hour_tick")
jsToken, err := common.GenerateSafeString(80) jsToken, err := common.GenerateSafeString(80)
if err != nil { if err != nil {
common.LogError(err) common.LogError(err)
} }
common.JSTokenBox.Store(jsToken) common.JSTokenBox.Store(jsToken)
common.OldSessionSigningKeyBox.Store(common.SessionSigningKeyBox.Load().(string)) // TODO: We probably don't need this type conversion
sessionSigningKey, err := common.GenerateSafeString(80)
if err != nil {
common.LogError(err)
}
common.SessionSigningKeyBox.Store(sessionSigningKey)
runTasks(common.ScheduledHourTasks) runTasks(common.ScheduledHourTasks)
runHook("after_hour_tick") runHook("after_hour_tick")
} }

View File

@ -476,171 +476,6 @@ func routePanelPluginsInstall(w http.ResponseWriter, r *http.Request, user commo
return nil return nil
} }
func routePanelUsers(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
header.Title = common.GetTitlePhrase("panel_users")
page, _ := strconv.Atoi(r.FormValue("page"))
perPage := 10
offset, page, lastPage := common.PageOffset(stats.Users, page, perPage)
users, err := common.Users.GetOffset(offset, perPage)
if err != nil {
return common.InternalError(err, w, r)
}
pageList := common.Paginate(stats.Users, perPage, 5)
pi := common.PanelUserPage{&common.BasePanelPage{header, stats, "users", common.ReportForumID}, users, common.Paginator{pageList, page, lastPage}}
return panelRenderTemplate("panel_users", w, r, user, &pi)
}
func routePanelUsersEdit(w http.ResponseWriter, r *http.Request, user common.User, suid string) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
if !user.Perms.EditUser {
return common.NoPermissions(w, r, user)
}
header.Title = common.GetTitlePhrase("panel_edit_user")
uid, err := strconv.Atoi(suid)
if err != nil {
return common.LocalError("The provided UserID is not a valid number.", w, r, user)
}
targetUser, err := common.Users.Get(uid)
if err == ErrNoRows {
return common.LocalError("The user you're trying to edit doesn't exist.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
if targetUser.IsAdmin && !user.IsAdmin {
return common.LocalError("Only administrators can edit the account of an administrator.", w, r, user)
}
// ? - Should we stop admins from deleting all the groups? Maybe, protect the group they're currently using?
groups, err := common.Groups.GetRange(1, 0) // ? - 0 = Go to the end
if err != nil {
return common.InternalError(err, w, r)
}
var groupList []interface{}
for _, group := range groups {
if !user.Perms.EditUserGroupAdmin && group.IsAdmin {
continue
}
if !user.Perms.EditUserGroupSuperMod && group.IsMod {
continue
}
groupList = append(groupList, group)
}
if r.FormValue("updated") == "1" {
header.AddNotice("panel_user_updated")
}
pi := common.PanelPage{&common.BasePanelPage{header, stats, "users", common.ReportForumID}, groupList, targetUser}
if common.RunPreRenderHook("pre_render_panel_edit_user", w, r, &user, &pi) {
return nil
}
err = common.Templates.ExecuteTemplate(w, "panel-user-edit.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, suid string) common.RouteError {
_, ferr := common.SimplePanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
if !user.Perms.EditUser {
return common.NoPermissions(w, r, user)
}
uid, err := strconv.Atoi(suid)
if err != nil {
return common.LocalError("The provided UserID is not a valid number.", w, r, user)
}
targetUser, err := common.Users.Get(uid)
if err == ErrNoRows {
return common.LocalError("The user you're trying to edit doesn't exist.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
if targetUser.IsAdmin && !user.IsAdmin {
return common.LocalError("Only administrators can edit the account of other administrators.", w, r, user)
}
newname := common.SanitiseSingleLine(r.PostFormValue("user-name"))
if newname == "" {
return common.LocalError("You didn't put in a username.", w, r, user)
}
// TODO: How should activation factor into admin set emails?
// TODO: How should we handle secondary emails? Do we even have secondary emails implemented?
newemail := common.SanitiseSingleLine(r.PostFormValue("user-email"))
if newemail == "" {
return common.LocalError("You didn't put in an email address.", w, r, user)
}
if (newemail != targetUser.Email) && !user.Perms.EditUserEmail {
return common.LocalError("You need the EditUserEmail permission to edit the email address of a user.", w, r, user)
}
newpassword := r.PostFormValue("user-password")
if newpassword != "" && !user.Perms.EditUserPassword {
return common.LocalError("You need the EditUserPassword permission to edit the password of a user.", w, r, user)
}
newgroup, err := strconv.Atoi(r.PostFormValue("user-group"))
if err != nil {
return common.LocalError("You need to provide a whole number for the group ID", w, r, user)
}
group, err := common.Groups.Get(newgroup)
if err == ErrNoRows {
return common.LocalError("The group you're trying to place this user in doesn't exist.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
if !user.Perms.EditUserGroupAdmin && group.IsAdmin {
return common.LocalError("You need the EditUserGroupAdmin permission to assign someone to an administrator group.", w, r, user)
}
if !user.Perms.EditUserGroupSuperMod && group.IsMod {
return common.LocalError("You need the EditUserGroupSuperMod permission to assign someone to a super mod group.", w, r, user)
}
// TODO: Move this query into common
_, err = stmts.updateUser.Exec(newname, newemail, newgroup, targetUser.ID)
if err != nil {
return common.InternalError(err, w, r)
}
if newpassword != "" {
common.SetPassword(targetUser.ID, newpassword)
// Log the user out as a safety precaution
common.Auth.ForceLogout(targetUser.ID)
}
targetUser.CacheRemove()
// If we're changing our own password, redirect to the index rather than to a noperms error due to the force logout
if targetUser.ID == user.ID {
http.Redirect(w, r, "/", http.StatusSeeOther)
} else {
http.Redirect(w, r, "/panel/users/edit/"+strconv.Itoa(targetUser.ID)+"?updated=1", http.StatusSeeOther)
}
return nil
}
func routePanelGroups(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func routePanelGroups(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) header, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil { if ferr != nil {
@ -748,7 +583,7 @@ func routePanelGroupsEdit(w http.ResponseWriter, r *http.Request, user common.Us
if common.RunPreRenderHook("pre_render_panel_edit_group", w, r, &user, &pi) { if common.RunPreRenderHook("pre_render_panel_edit_group", w, r, &user, &pi) {
return nil return nil
} }
err = common.Templates.ExecuteTemplate(w, "panel-group-edit.html", pi) err = common.Templates.ExecuteTemplate(w, "panel_group_edit.html", pi)
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
@ -835,7 +670,7 @@ func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user comm
if common.RunPreRenderHook("pre_render_panel_edit_group_perms", w, r, &user, &pi) { if common.RunPreRenderHook("pre_render_panel_edit_group_perms", w, r, &user, &pi) {
return nil return nil
} }
err = common.Templates.ExecuteTemplate(w, "panel-group-edit-perms.html", pi) err = common.Templates.ExecuteTemplate(w, "panel_group_edit_perms.html", pi)
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }

View File

@ -11,7 +11,6 @@ import (
"strconv" "strconv"
"../common" "../common"
"../config"
"../query_gen/lib" "../query_gen/lib"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
) )
@ -37,12 +36,23 @@ func main() {
} }
}() }()
config.Config() log.Print("Loading the configuration data")
err := common.LoadConfig()
if err != nil {
log.Fatal(err)
}
log.Print("Processing configuration data")
err = common.ProcessConfig()
if err != nil {
log.Fatal(err)
}
if common.DbConfig.Adapter != "mysql" && common.DbConfig.Adapter != "" { if common.DbConfig.Adapter != "mysql" && common.DbConfig.Adapter != "" {
log.Fatal("Only MySQL is supported for upgrades right now, please wait for a newer build of the patcher") log.Fatal("Only MySQL is supported for upgrades right now, please wait for a newer build of the patcher")
} }
err := prepMySQL() err = prepMySQL()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -13,6 +13,7 @@ func init() {
addPatch(2, patch2) addPatch(2, patch2)
addPatch(3, patch3) addPatch(3, patch3)
addPatch(4, patch4) addPatch(4, patch4)
addPatch(5, patch5)
} }
func patch0(scanner *bufio.Scanner) (err error) { func patch0(scanner *bufio.Scanner) (err error) {
@ -447,3 +448,64 @@ func patch4(scanner *bufio.Scanner) error {
return nil return nil
} }
func patch5(scanner *bufio.Scanner) error {
// ! Don't reuse this function blindly, it doesn't escape apostrophes
var replaceTextWhere = func(replaceThis string, withThis string) error {
return execStmt(qgen.Builder.SimpleUpdate("viewchunks", "route = '"+withThis+"'", "route = '"+replaceThis+"'"))
}
err := replaceTextWhere("routePanelUsers", "panel.Users")
if err != nil {
return err
}
err = replaceTextWhere("routePanelUsersEdit", "panel.UsersEdit")
if err != nil {
return err
}
err = replaceTextWhere("routePanelUsersEditSubmit", "panel.UsersEditSubmit")
if err != nil {
return err
}
err = replaceTextWhere("routes.AccountEditCritical", "routes.AccountEditPassword")
if err != nil {
return err
}
err = replaceTextWhere("routes.AccountEditCriticalSubmit", "routes.AccountEditPasswordSubmit")
if err != nil {
return err
}
err = execStmt(qgen.Builder.SimpleUpdate("menu_items", "path = '/user/edit/'", "path = '/user/edit/critical/'"))
if err != nil {
return err
}
err = execStmt(qgen.Builder.CreateTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"uid", "int", 0, false, false, ""},
qgen.DBTableColumn{"secret", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"scratch1", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch2", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch3", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch4", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch5", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch6", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch7", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch8", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""},
},
[]qgen.DBTableKey{
qgen.DBTableKey{"uid", "primary"},
},
))
if err != nil {
return err
}
return nil
}

View File

@ -343,11 +343,11 @@ func bbcodeParseURL(i int, start int, lastTag int, msgbytes []byte, outbytes []b
return i, start, lastTag, outbytes return i, start, lastTag, outbytes
} }
outbytes = append(outbytes, common.UrlOpen...) outbytes = append(outbytes, common.URLOpen...)
outbytes = append(outbytes, msgbytes[start:i]...) outbytes = append(outbytes, msgbytes[start:i]...)
outbytes = append(outbytes, common.UrlOpen2...) outbytes = append(outbytes, common.URLOpen2...)
outbytes = append(outbytes, msgbytes[start:i]...) outbytes = append(outbytes, msgbytes[start:i]...)
outbytes = append(outbytes, common.UrlClose...) outbytes = append(outbytes, common.URLClose...)
i += 6 i += 6
lastTag = i lastTag = i

7
public/account.js Normal file
View File

@ -0,0 +1,7 @@
"use strict"
$(document).ready(function(){
$("#dash_username input").click(function(){
$("#dash_username button").show();
});
});

View File

@ -495,8 +495,11 @@ $(document).ready(function(){
files[i] = fileList[i]; files[i] = fileList[i];
// Iterate over the files // Iterate over the files
let totalSize = 0;
for(let i = 0; i < files.length; i++) { for(let i = 0; i < files.length; i++) {
console.log("files[" + i + "]",files[i]); console.log("files[" + i + "]",files[i]);
totalSize += files[i]["size"];
let reader = new FileReader(); let reader = new FileReader();
reader.onload = function(e) { reader.onload = function(e) {
var fileDock = document.getElementById("upload_file_dock"); var fileDock = document.getElementById("upload_file_dock");
@ -540,6 +543,9 @@ $(document).ready(function(){
} }
reader.readAsDataURL(files[i]); reader.readAsDataURL(files[i]);
} }
if(totalSize>maxRequestSize) {
alert("You can't upload this much data at once, max: " + maxRequestSize);
}
} }
var uploadFiles = document.getElementById("upload_files"); var uploadFiles = document.getElementById("upload_files");

View File

@ -225,7 +225,7 @@ func seedTables(adapter qgen.Adapter) error {
addMenuItem(map[string]interface{}{"mid": 1, "htmlID": "general_alerts", "cssClass": "menu_alerts", "position": "right", "tmplName": "menu_alerts"}) addMenuItem(map[string]interface{}{"mid": 1, "htmlID": "general_alerts", "cssClass": "menu_alerts", "position": "right", "tmplName": "menu_alerts"})
addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_account}", "cssClass": "menu_account", "position": "left", "path": "/user/edit/critical/", "aria": "{lang.menu_account_aria}", "tooltip": "{lang.menu_account_tooltip}", "memberOnly": true}) addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_account}", "cssClass": "menu_account", "position": "left", "path": "/user/edit/", "aria": "{lang.menu_account_aria}", "tooltip": "{lang.menu_account_tooltip}", "memberOnly": true})
addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_profile}", "cssClass": "menu_profile", "position": "left", "path": "{me.Link}", "aria": "{lang.menu_profile_aria}", "tooltip": "{lang.menu_profile_tooltip}", "memberOnly": true}) addMenuItem(map[string]interface{}{"mid": 1, "name": "{lang.menu_profile}", "cssClass": "menu_profile", "position": "left", "path": "{me.Link}", "aria": "{lang.menu_profile_aria}", "tooltip": "{lang.menu_profile_tooltip}", "memberOnly": true})
@ -301,8 +301,6 @@ func writeUpdates(adapter qgen.Adapter) error {
build.Update("updateTheme").Table("themes").Set("default = ?").Where("uname = ?").Parse() build.Update("updateTheme").Table("themes").Set("default = ?").Where("uname = ?").Parse()
build.Update("updateUser").Table("users").Set("name = ?, email = ?, group = ?").Where("uid = ?").Parse() // TODO: Implement user_count for users_groups on things which use this
build.Update("updateGroupPerms").Table("users_groups").Set("permissions = ?").Where("gid = ?").Parse() build.Update("updateGroupPerms").Table("users_groups").Set("permissions = ?").Where("gid = ?").Parse()
build.Update("updateGroup").Table("users_groups").Set("name = ?, tag = ?").Where("gid = ?").Parse() build.Update("updateGroup").Table("users_groups").Set("name = ?, tag = ?").Where("gid = ?").Parse()

View File

@ -62,25 +62,24 @@ func createTables(adapter qgen.Adapter) error {
}, },
) )
/* qgen.Install.CreateTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci",
qgen.Install.CreateTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci", []qgen.DBTableColumn{
[]qgen.DBTableColumn{ qgen.DBTableColumn{"uid", "int", 0, false, false, ""},
qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, qgen.DBTableColumn{"secret", "varchar", 100, false, false, ""},
qgen.DBTableColumn{"secret", "varchar", 100, false, false, ""}, qgen.DBTableColumn{"scratch1", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch1", "varchar", 50, false, false, ""}, qgen.DBTableColumn{"scratch2", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch2", "varchar", 50, false, false, ""}, qgen.DBTableColumn{"scratch3", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch3", "varchar", 50, false, false, ""}, qgen.DBTableColumn{"scratch4", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch4", "varchar", 50, false, false, ""}, qgen.DBTableColumn{"scratch5", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch5", "varchar", 50, false, false, ""}, qgen.DBTableColumn{"scratch6", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch6", "varchar", 50, false, false, ""}, qgen.DBTableColumn{"scratch7", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch7", "varchar", 50, false, false, ""}, qgen.DBTableColumn{"scratch8", "varchar", 50, false, false, ""},
qgen.DBTableColumn{"scratch8", "varchar", 50, false, false, ""}, qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""},
}, },
[]qgen.DBTableKey{ []qgen.DBTableKey{
qgen.DBTableKey{"uid", "primary"}, qgen.DBTableKey{"uid", "primary"},
}, },
) )
*/
// What should we do about global penalties? Put them on the users table for speed? Or keep them here? // What should we do about global penalties? Put them on the users table for speed? Or keep them here?
// Should we add IP Penalties? No, that's a stupid idea, just implement IP Bans properly. What about shadowbans? // Should we add IP Penalties? No, that's a stupid idea, just implement IP Bans properly. What about shadowbans?

View File

@ -416,9 +416,9 @@ func (router *GenRouter) SuspiciousRequest(req *http.Request, prepend string) {
counters.AgentViewCounter.Bump({{.AllAgentMap.suspicious}}) counters.AgentViewCounter.Bump({{.AllAgentMap.suspicious}})
} }
// TODO: Pass the default route or config struct to the router rather than accessing it via a package global // TODO: Pass the default path or config struct to the router rather than accessing it via a package global
// TODO: SetDefaultRoute // TODO: SetDefaultPath
// TODO: GetDefaultRoute // TODO: GetDefaultPath
func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Redirect www. requests to the right place // Redirect www. requests to the right place
if req.Host == "www." + common.Site.Host { if req.Host == "www." + common.Site.Host {
@ -456,6 +456,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if strings.Contains(req.URL.Path,"..") || strings.Contains(req.URL.Path,"--") || strings.Contains(lowerPath,".php") || strings.Contains(lowerPath,".asp") || strings.Contains(lowerPath,".cgi") || strings.Contains(lowerPath,".py") || strings.Contains(lowerPath,".sql") || strings.Contains(lowerPath,".action") { if strings.Contains(req.URL.Path,"..") || strings.Contains(req.URL.Path,"--") || strings.Contains(lowerPath,".php") || strings.Contains(lowerPath,".asp") || strings.Contains(lowerPath,".cgi") || strings.Contains(lowerPath,".py") || strings.Contains(lowerPath,".sql") || strings.Contains(lowerPath,".action") {
router.SuspiciousRequest(req,"") router.SuspiciousRequest(req,"")
} }
// Indirect the default route onto a different one
if req.URL.Path == "/" {
req.URL.Path = common.Config.DefaultPath
}
var prefix, extraData string var prefix, extraData string
prefix = req.URL.Path[0:strings.IndexByte(req.URL.Path[1:],'/') + 1] prefix = req.URL.Path[0:strings.IndexByte(req.URL.Path[1:],'/') + 1]
@ -671,21 +676,8 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
return*/ return*/
} }
if extraData != "" { common.NotFound(w,req,nil)
common.NotFound(w,req,nil) return
return
}
handle, ok := RouteMap[common.Config.DefaultRoute]
if !ok {
// TODO: Make this a startup error not a runtime one
router.requestLogger.Print("Unable to find the default route")
common.NotFound(w,req,nil)
return
}
counters.RouteViewCounter.Bump(routeMapEnum[common.Config.DefaultRoute])
handle.(func(http.ResponseWriter, *http.Request, common.User) common.RouteError)(w,req,user)
default: default:
// A fallback for the routes which haven't been converted to the new router yet or plugins // A fallback for the routes which haven't been converted to the new router yet or plugins
router.RLock() router.RLock()

View File

@ -40,12 +40,15 @@ func buildUserRoutes() {
userGroup := newRouteGroup("/user/") userGroup := newRouteGroup("/user/")
userGroup.Routes( userGroup.Routes(
View("routes.ViewProfile", "/user/").LitBefore("req.URL.Path += extraData"), View("routes.ViewProfile", "/user/").LitBefore("req.URL.Path += extraData"),
MemberView("routes.AccountEditCritical", "/user/edit/critical/"), MemberView("routes.AccountEdit", "/user/edit/"),
Action("routes.AccountEditCriticalSubmit", "/user/edit/critical/submit/"), // TODO: Full test this MemberView("routes.AccountEditPassword", "/user/edit/password/"),
MemberView("routes.AccountEditAvatar", "/user/edit/avatar/"), Action("routes.AccountEditPasswordSubmit", "/user/edit/password/submit/"), // TODO: Full test this
UploadAction("routes.AccountEditAvatarSubmit", "/user/edit/avatar/submit/").MaxSizeVar("int(common.Config.MaxRequestSize)"), UploadAction("routes.AccountEditAvatarSubmit", "/user/edit/avatar/submit/").MaxSizeVar("int(common.Config.MaxRequestSize)"),
MemberView("routes.AccountEditUsername", "/user/edit/username/"),
Action("routes.AccountEditUsernameSubmit", "/user/edit/username/submit/"), // TODO: Full test this Action("routes.AccountEditUsernameSubmit", "/user/edit/username/submit/"), // TODO: Full test this
MemberView("routes.AccountEditMFA", "/user/edit/mfa/"),
MemberView("routes.AccountEditMFASetup", "/user/edit/mfa/setup/"),
Action("routes.AccountEditMFASetupSubmit", "/user/edit/mfa/setup/submit/"),
Action("routes.AccountEditMFADisableSubmit", "/user/edit/mfa/disable/submit/"),
MemberView("routes.AccountEditEmail", "/user/edit/email/"), MemberView("routes.AccountEditEmail", "/user/edit/email/"),
Action("routes.AccountEditEmailTokenSubmit", "/user/edit/token/", "extraData"), Action("routes.AccountEditEmailTokenSubmit", "/user/edit/token/", "extraData"),
) )
@ -95,7 +98,6 @@ func buildReplyRoutes() {
// TODO: Move these into /user/? // TODO: Move these into /user/?
func buildProfileReplyRoutes() { func buildProfileReplyRoutes() {
//router.HandleFunc("/user/edit/submit/", routeLogout) // routeLogout? what on earth? o.o
pReplyGroup := newRouteGroup("/profile/") pReplyGroup := newRouteGroup("/profile/")
pReplyGroup.Routes( pReplyGroup.Routes(
Action("routes.ProfileReplyCreateSubmit", "/profile/reply/create/"), // TODO: Add /submit/ to the end Action("routes.ProfileReplyCreateSubmit", "/profile/reply/create/"), // TODO: Add /submit/ to the end
@ -122,6 +124,8 @@ func buildAccountRoutes() {
View("routes.AccountRegister", "/accounts/create/"), View("routes.AccountRegister", "/accounts/create/"),
Action("routes.AccountLogout", "/accounts/logout/"), Action("routes.AccountLogout", "/accounts/logout/"),
AnonAction("routes.AccountLoginSubmit", "/accounts/login/submit/"), // TODO: Guard this with a token, maybe the IP hashed with a rotated key? AnonAction("routes.AccountLoginSubmit", "/accounts/login/submit/"), // TODO: Guard this with a token, maybe the IP hashed with a rotated key?
View("routes.AccountLoginMFAVerify", "/accounts/mfa_verify/"),
AnonAction("routes.AccountLoginMFAVerifySubmit", "/accounts/mfa_verify/submit/"), // We have logic in here which filters out regular guests
AnonAction("routes.AccountRegisterSubmit", "/accounts/create/submit/"), AnonAction("routes.AccountRegisterSubmit", "/accounts/create/submit/"),
) )
addRouteGroup(accReplyGroup) addRouteGroup(accReplyGroup)
@ -172,9 +176,9 @@ func buildPanelRoutes() {
Action("routePanelPluginsDeactivate", "/panel/plugins/deactivate/", "extraData"), Action("routePanelPluginsDeactivate", "/panel/plugins/deactivate/", "extraData"),
Action("routePanelPluginsInstall", "/panel/plugins/install/", "extraData"), Action("routePanelPluginsInstall", "/panel/plugins/install/", "extraData"),
View("routePanelUsers", "/panel/users/"), View("panel.Users", "/panel/users/"),
View("routePanelUsersEdit", "/panel/users/edit/", "extraData"), View("panel.UsersEdit", "/panel/users/edit/", "extraData"),
Action("routePanelUsersEditSubmit", "/panel/users/edit/submit/", "extraData"), Action("panel.UsersEditSubmit", "/panel/users/edit/submit/", "extraData"),
View("panel.AnalyticsViews", "/panel/analytics/views/").Before("ParseForm"), View("panel.AnalyticsViews", "/panel/analytics/views/").Before("ParseForm"),
View("panel.AnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"), View("panel.AnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"),

View File

@ -2,6 +2,7 @@ package routes
import ( import (
"crypto/sha256" "crypto/sha256"
"crypto/subtle"
"database/sql" "database/sql"
"encoding/hex" "encoding/hex"
"io" "io"
@ -48,16 +49,30 @@ func AccountLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User
} }
username := common.SanitiseSingleLine(r.PostFormValue("username")) username := common.SanitiseSingleLine(r.PostFormValue("username"))
uid, err := common.Auth.Authenticate(username, r.PostFormValue("password")) uid, err, requiresExtraAuth := common.Auth.Authenticate(username, r.PostFormValue("password"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) return common.LocalError(err.Error(), w, r, user)
} }
// 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)
}
common.Auth.SetProvisionalCookies(w, uid, provSession, signedSession)
http.Redirect(w, r, "/accounts/mfa_verify/", http.StatusSeeOther)
return nil
}
return loginSuccess(uid, w, r, &user)
}
func loginSuccess(uid int, w http.ResponseWriter, r *http.Request, user *common.User) common.RouteError {
userPtr, err := common.Users.Get(uid) userPtr, err := common.Users.Get(uid)
if err != nil { if err != nil {
return common.LocalError("Bad account", w, r, user) return common.LocalError("Bad account", w, r, *user)
} }
user = *userPtr *user = *userPtr
var session string var session string
if user.Session == "" { if user.Session == "" {
@ -79,6 +94,97 @@ func AccountLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User
return nil return nil
} }
func extractCookie(name string, r *http.Request) (string, error) {
cookie, err := r.Cookie(name)
if err != nil {
return "", err
}
return cookie.Value, nil
}
func mfaGetCookies(r *http.Request) (uid int, provSession string, signedSession string, err error) {
suid, err := extractCookie("uid", r)
if err != nil {
return 0, "", "", err
}
uid, err = strconv.Atoi(suid)
if err != nil {
return 0, "", "", err
}
provSession, err = extractCookie("provSession", r)
if err != nil {
return 0, "", "", err
}
signedSession, err = extractCookie("signedSession", r)
return uid, provSession, signedSession, err
}
func mfaVerifySession(provSession string, signedSession string, uid int) bool {
h := sha256.New()
h.Write([]byte(common.SessionSigningKeyBox.Load().(string)))
h.Write([]byte(provSession))
h.Write([]byte(strconv.Itoa(uid)))
expected := hex.EncodeToString(h.Sum(nil))
if subtle.ConstantTimeCompare([]byte(signedSession), []byte(expected)) == 1 {
return true
}
h = sha256.New()
h.Write([]byte(common.OldSessionSigningKeyBox.Load().(string)))
h.Write([]byte(provSession))
h.Write([]byte(strconv.Itoa(uid)))
expected = hex.EncodeToString(h.Sum(nil))
return subtle.ConstantTimeCompare([]byte(signedSession), []byte(expected)) == 1
}
func AccountLoginMFAVerify(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
if user.Loggedin {
return common.LocalError("You're already logged in.", w, r, user)
}
header.Title = common.GetTitlePhrase("login_mfa_verify")
uid, provSession, signedSession, err := mfaGetCookies(r)
if err != nil {
return common.LocalError("Invalid cookie", w, r, user)
}
if !mfaVerifySession(provSession, signedSession, uid) {
return common.LocalError("Invalid session", w, r, user)
}
pi := common.Page{header, tList, nil}
if common.RunPreRenderHook("pre_render_login_mfa_verify", w, r, &user, &pi) {
return nil
}
err = common.RunThemeTemplate(header.Theme.Name, "login_mfa_verify", pi, w)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func AccountLoginMFAVerifySubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
uid, provSession, signedSession, err := mfaGetCookies(r)
if err != nil {
return common.LocalError("Invalid cookie", w, r, user)
}
if !mfaVerifySession(provSession, signedSession, uid) {
return common.LocalError("Invalid session", w, r, user)
}
var token = r.PostFormValue("mfa_token")
err = common.Auth.ValidateMFAToken(token, uid)
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
return loginSuccess(uid, w, r, &user)
}
func AccountLogout(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AccountLogout(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
common.Auth.Logout(w, user.ID) common.Auth.Logout(w, user.ID)
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
@ -233,14 +339,57 @@ func AccountRegisterSubmit(w http.ResponseWriter, r *http.Request, user common.U
return nil return nil
} }
// TODO: Rename this // TODO: Figure a way of making this into middleware?
func AccountEditCritical(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func accountEditHead(titlePhrase string, w http.ResponseWriter, r *http.Request, user *common.User) (*common.Header, common.RouteError) {
header, ferr := common.UserCheck(w, r, &user) header, ferr := common.UserCheck(w, r, user)
if ferr != nil {
return nil, ferr
}
header.Title = common.GetTitlePhrase(titlePhrase)
header.AddSheet(header.Theme.Name + "/account.css")
header.AddScript("account.js")
return header, nil
}
func AccountEdit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, ferr := accountEditHead("account", w, r, &user)
if ferr != nil {
return ferr
}
if r.FormValue("avatar_updated") == "1" {
header.AddNotice("account_avatar_updated")
} else if r.FormValue("username_updated") == "1" {
header.AddNotice("account_username_updated")
} else if r.FormValue("mfa_setup_success") == "1" {
header.AddNotice("account_mfa_setup_success")
}
// TODO: Find a more efficient way of doing this
var mfaSetup = false
_, err := common.MFAstore.Get(user.ID)
if err != sql.ErrNoRows && err != nil {
return common.InternalError(err, w, r)
} else if err != sql.ErrNoRows {
mfaSetup = true
}
pi := common.AccountDashPage{header, mfaSetup}
if common.RunPreRenderHook("pre_render_account_own_edit", w, r, &user, &pi) {
return nil
}
err = common.Templates.ExecuteTemplate(w, "account_own_edit.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func AccountEditPassword(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, ferr := accountEditHead("account_password", w, r, &user)
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
// TODO: Add a phrase for this
header.Title = "Edit Password"
pi := common.Page{header, tList, nil} pi := common.Page{header, tList, nil}
if common.RunPreRenderHook("pre_render_account_own_edit_password", w, r, &user, &pi) { if common.RunPreRenderHook("pre_render_account_own_edit_password", w, r, &user, &pi) {
@ -253,8 +402,8 @@ func AccountEditCritical(w http.ResponseWriter, r *http.Request, user common.Use
return nil return nil
} }
// TODO: Rename this // TODO: Require re-authentication if the user hasn't logged in in a while
func AccountEditCriticalSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AccountEditPasswordSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
_, ferr := common.SimpleUserCheck(w, r, &user) _, ferr := common.SimpleUserCheck(w, r, &user)
if ferr != nil { if ferr != nil {
return ferr return ferr
@ -291,27 +440,6 @@ func AccountEditCriticalSubmit(w http.ResponseWriter, r *http.Request, user comm
return nil return nil
} }
func AccountEditAvatar(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
header.Title = common.GetTitlePhrase("account_avatar")
if r.FormValue("updated") == "1" {
header.AddNotice("account_avatar_updated")
}
pi := common.Page{header, tList, nil}
if common.RunPreRenderHook("pre_render_account_own_edit_avatar", w, r, &user, &pi) {
return nil
}
err := common.Templates.ExecuteTemplate(w, "account_own_edit_avatar.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func AccountEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AccountEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
_, ferr := common.SimpleUserCheck(w, r, &user) _, ferr := common.SimpleUserCheck(w, r, &user)
if ferr != nil { if ferr != nil {
@ -377,28 +505,7 @@ func AccountEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user common
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
http.Redirect(w, r, "/user/edit/avatar/?updated=1", http.StatusSeeOther) http.Redirect(w, r, "/user/edit/?avatar_updated=1", http.StatusSeeOther)
return nil
}
func AccountEditUsername(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, ferr := common.UserCheck(w, r, &user)
if ferr != nil {
return ferr
}
header.Title = common.GetTitlePhrase("account_username")
if r.FormValue("updated") == "1" {
header.AddNotice("account_username_updated")
}
pi := common.Page{header, tList, user.Name}
if common.RunPreRenderHook("pre_render_account_own_edit_username", w, r, &user, &pi) {
return nil
}
err := common.Templates.ExecuteTemplate(w, "account_own_edit_username.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil return nil
} }
@ -409,22 +516,140 @@ func AccountEditUsernameSubmit(w http.ResponseWriter, r *http.Request, user comm
} }
newUsername := common.SanitiseSingleLine(r.PostFormValue("account-new-username")) newUsername := common.SanitiseSingleLine(r.PostFormValue("account-new-username"))
if newUsername == "" {
return common.LocalError("You can't leave your username blank", w, r, user)
}
err := user.ChangeName(newUsername) err := user.ChangeName(newUsername)
if err != nil { if err != nil {
return common.LocalError("Unable to change the username. Does someone else already have this name?", w, r, user) return common.LocalError("Unable to change the username. Does someone else already have this name?", w, r, user)
} }
http.Redirect(w, r, "/user/edit/username/?updated=1", http.StatusSeeOther) http.Redirect(w, r, "/user/edit/?username_updated=1", http.StatusSeeOther)
return nil
}
func AccountEditMFA(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, ferr := accountEditHead("account_mfa", w, r, &user)
if ferr != nil {
return ferr
}
mfaItem, err := common.MFAstore.Get(user.ID)
if err != sql.ErrNoRows && err != nil {
return common.InternalError(err, w, r)
} else if err == sql.ErrNoRows {
return common.LocalError("Two-factor authentication hasn't been setup on your account", w, r, user)
}
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
}
// If not setup, generate a string, otherwise give an option to disable mfa given the right code
func AccountEditMFASetup(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, ferr := accountEditHead("account_mfa_setup", w, r, &user)
if ferr != nil {
return ferr
}
// Flash an error if mfa is already setup
_, err := common.MFAstore.Get(user.ID)
if err != sql.ErrNoRows && err != nil {
return common.InternalError(err, w, r)
} else if err != sql.ErrNoRows {
return common.LocalError("You have already setup two-factor authentication", w, r, user)
}
// TODO: Entitise this?
code, err := common.GenerateGAuthSecret()
if err != nil {
return common.InternalError(err, w, r)
}
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
}
// 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
func AccountEditMFASetupSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
_, ferr := common.SimpleUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
// Flash an error if mfa is already setup
_, err := common.MFAstore.Get(user.ID)
if err != sql.ErrNoRows && err != nil {
return common.InternalError(err, w, r)
} else if err != sql.ErrNoRows {
return common.LocalError("You have already setup two-factor authentication", w, r, user)
}
var code = r.PostFormValue("code")
var otp = r.PostFormValue("otp")
ok, err := common.VerifyGAuthToken(code, otp)
if err != nil {
//fmt.Println("err: ", err)
return common.LocalError("Something weird happened", w, r, user) // TODO: Log this error?
}
// TODO: Use AJAX for this
if !ok {
return common.LocalError("The token isn't right", w, r, user)
}
// TODO: How should we handle races where a mfa key is already setup? Right now, it's a fairly generic error, maybe try parsing the error message?
err = common.MFAstore.Create(code, user.ID)
if err != nil {
return common.InternalError(err, w, r)
}
http.Redirect(w, r, "/user/edit/?mfa_setup_success=1", http.StatusSeeOther)
return nil
}
// TODO: Implement this
func AccountEditMFADisableSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
_, ferr := common.SimpleUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
// Flash an error if mfa is already setup
mfaItem, err := common.MFAstore.Get(user.ID)
if err != sql.ErrNoRows && err != nil {
return common.InternalError(err, w, r)
} else if err == sql.ErrNoRows {
return common.LocalError("You don't have two-factor enabled on your account", w, r, user)
}
err = mfaItem.Delete()
if err != nil {
return common.InternalError(err, w, r)
}
http.Redirect(w, r, "/user/edit/?mfa_disabled=1", http.StatusSeeOther)
return nil return nil
} }
func AccountEditEmail(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AccountEditEmail(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, ferr := common.UserCheck(w, r, &user) header, ferr := accountEditHead("account_email", w, r, &user)
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("account_email")
emails, err := common.Emails.GetEmailsByUser(&user) emails, err := common.Emails.GetEmailsByUser(&user)
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)

View File

@ -108,17 +108,15 @@ func analyticsRowsToViewMap(rows *sql.Rows, labelList []int64, viewMap map[int64
} }
func PreAnalyticsDetail(w http.ResponseWriter, r *http.Request, user *common.User) (*common.BasePanelPage, common.RouteError) { func PreAnalyticsDetail(w http.ResponseWriter, r *http.Request, user *common.User) (*common.BasePanelPage, common.RouteError) {
header, stats, ferr := common.PanelUserCheck(w, r, user) basePage, ferr := buildBasePage(w, r, user, "analytics", "analytics")
if ferr != nil { if ferr != nil {
return nil, ferr return nil, ferr
} }
header.Title = common.GetTitlePhrase("panel_analytics") basePage.AddSheet("chartist/chartist.min.css")
header.AddSheet("chartist/chartist.min.css") basePage.AddScript("chartist/chartist.min.js")
header.AddScript("chartist/chartist.min.js") basePage.AddScript("analytics.js")
header.AddScript("analytics.js") return basePage, nil
return &common.BasePanelPage{header, stats, "analytics", common.ReportForumID}, nil
} }
func AnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
@ -497,12 +495,10 @@ func analyticsRowsToNameMap(rows *sql.Rows) (map[string]int, error) {
} }
func AnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) return common.LocalError(err.Error(), w, r, user)
@ -537,17 +533,15 @@ func AnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) c
}) })
} }
pi := common.PanelAnalyticsAgentsPage{&common.BasePanelPage{header, stats, "analytics", common.ReportForumID}, forumItems, timeRange.Range} pi := common.PanelAnalyticsAgentsPage{basePage, forumItems, timeRange.Range}
return panelRenderTemplate("panel_analytics_forums", w, r, user, &pi) return panelRenderTemplate("panel_analytics_forums", w, r, user, &pi)
} }
func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) return common.LocalError(err.Error(), w, r, user)
@ -573,17 +567,15 @@ func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) c
}) })
} }
pi := common.PanelAnalyticsRoutesPage{&common.BasePanelPage{header, stats, "analytics", common.ReportForumID}, routeItems, timeRange.Range} pi := common.PanelAnalyticsRoutesPage{basePage, routeItems, timeRange.Range}
return panelRenderTemplate("panel_analytics_routes", w, r, user, &pi) return panelRenderTemplate("panel_analytics_routes", w, r, user, &pi)
} }
func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) return common.LocalError(err.Error(), w, r, user)
@ -614,17 +606,15 @@ func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) c
}) })
} }
pi := common.PanelAnalyticsAgentsPage{&common.BasePanelPage{header, stats, "analytics", common.ReportForumID}, agentItems, timeRange.Range} pi := common.PanelAnalyticsAgentsPage{basePage, agentItems, timeRange.Range}
return panelRenderTemplate("panel_analytics_agents", w, r, user, &pi) return panelRenderTemplate("panel_analytics_agents", w, r, user, &pi)
} }
func AnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) return common.LocalError(err.Error(), w, r, user)
@ -655,17 +645,15 @@ func AnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User)
}) })
} }
pi := common.PanelAnalyticsAgentsPage{&common.BasePanelPage{header, stats, "analytics", common.ReportForumID}, systemItems, timeRange.Range} pi := common.PanelAnalyticsAgentsPage{basePage, systemItems, timeRange.Range}
return panelRenderTemplate("panel_analytics_systems", w, r, user, &pi) return panelRenderTemplate("panel_analytics_systems", w, r, user, &pi)
} }
func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) return common.LocalError(err.Error(), w, r, user)
@ -697,17 +685,15 @@ func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User
}) })
} }
pi := common.PanelAnalyticsAgentsPage{&common.BasePanelPage{header, stats, "analytics", common.ReportForumID}, langItems, timeRange.Range} pi := common.PanelAnalyticsAgentsPage{basePage, langItems, timeRange.Range}
return panelRenderTemplate("panel_analytics_langs", w, r, user, &pi) return panelRenderTemplate("panel_analytics_langs", w, r, user, &pi)
} }
func AnalyticsReferrers(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AnalyticsReferrers(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) return common.LocalError(err.Error(), w, r, user)
@ -733,6 +719,6 @@ func AnalyticsReferrers(w http.ResponseWriter, r *http.Request, user common.User
}) })
} }
pi := common.PanelAnalyticsAgentsPage{&common.BasePanelPage{header, stats, "analytics", common.ReportForumID}, refItems, timeRange.Range} pi := common.PanelAnalyticsAgentsPage{basePage, refItems, timeRange.Range}
return panelRenderTemplate("panel_analytics_referrers", w, r, user, &pi) return panelRenderTemplate("panel_analytics_referrers", w, r, user, &pi)
} }

View File

@ -11,11 +11,10 @@ import (
) )
func Backups(w http.ResponseWriter, r *http.Request, user common.User, backupURL string) common.RouteError { func Backups(w http.ResponseWriter, r *http.Request, user common.User, backupURL string) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "backups", "backups")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_backups")
if backupURL != "" { if backupURL != "" {
// We don't want them trying to break out of this directory, it shouldn't hurt since it's a super admin, but it's always good to practice good security hygiene, especially if this is one of many instances on a managed server not controlled by the superadmin/s // We don't want them trying to break out of this directory, it shouldn't hurt since it's a super admin, but it's always good to practice good security hygiene, especially if this is one of many instances on a managed server not controlled by the superadmin/s
@ -25,7 +24,7 @@ func Backups(w http.ResponseWriter, r *http.Request, user common.User, backupURL
if ext == ".sql" { if ext == ".sql" {
info, err := os.Stat("./backups/" + backupURL) info, err := os.Stat("./backups/" + backupURL)
if err != nil { if err != nil {
return common.NotFound(w, r, header) return common.NotFound(w, r, basePage.Header)
} }
// TODO: Change the served filename to gosora_backup_%timestamp%.sql, the time the file was generated, not when it was modified aka what the name of it should be // TODO: Change the served filename to gosora_backup_%timestamp%.sql, the time the file was generated, not when it was modified aka what the name of it should be
w.Header().Set("Content-Disposition", "attachment; filename=gosora_backup.sql") w.Header().Set("Content-Disposition", "attachment; filename=gosora_backup.sql")
@ -34,7 +33,7 @@ func Backups(w http.ResponseWriter, r *http.Request, user common.User, backupURL
http.ServeFile(w, r, "./backups/"+backupURL) http.ServeFile(w, r, "./backups/"+backupURL)
return nil return nil
} }
return common.NotFound(w, r, header) return common.NotFound(w, r, basePage.Header)
} }
var backupList []common.BackupItem var backupList []common.BackupItem
@ -50,6 +49,6 @@ func Backups(w http.ResponseWriter, r *http.Request, user common.User, backupURL
backupList = append(backupList, common.BackupItem{backupFile.Name(), backupFile.ModTime()}) backupList = append(backupList, common.BackupItem{backupFile.Name(), backupFile.ModTime()})
} }
pi := common.PanelBackupPage{&common.BasePanelPage{header, stats, "backups", common.ReportForumID}, backupList} pi := common.PanelBackupPage{basePage, backupList}
return panelRenderTemplate("panel_backups", w, r, user, &pi) return panelRenderTemplate("panel_backups", w, r, user, &pi)
} }

View File

@ -29,3 +29,13 @@ func panelRenderTemplate(tmplName string, w http.ResponseWriter, r *http.Request
} }
return nil return nil
} }
func buildBasePage(w http.ResponseWriter, r *http.Request, user *common.User, titlePhrase string, zone string) (*common.BasePanelPage, common.RouteError) {
header, stats, ferr := common.PanelUserCheck(w, r, user)
if ferr != nil {
return nil, ferr
}
header.Title = common.GetTitlePhrase("panel_" + titlePhrase)
return &common.BasePanelPage{header, stats, zone, common.ReportForumID}, nil
}

View File

@ -11,11 +11,10 @@ import (
) )
func Debug(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func Debug(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "debug", "debug")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_debug")
goVersion := runtime.Version() goVersion := runtime.Version()
dbVersion := qgen.Builder.DbVersion() dbVersion := qgen.Builder.DbVersion()
@ -38,6 +37,6 @@ func Debug(w http.ResponseWriter, r *http.Request, user common.User) common.Rout
// Disk I/O? // Disk I/O?
// TODO: Fetch the adapter from Builder rather than getting it from a global? // TODO: Fetch the adapter from Builder rather than getting it from a global?
pi := common.PanelDebugPage{&common.BasePanelPage{header, stats, "debug", common.ReportForumID}, goVersion, dbVersion, uptime, openConnCount, qgen.Builder.GetAdapter().GetName()} pi := common.PanelDebugPage{basePage, goVersion, dbVersion, uptime, openConnCount, qgen.Builder.GetAdapter().GetName()}
return panelRenderTemplate("panel_debug", w, r, user, &pi) return panelRenderTemplate("panel_debug", w, r, user, &pi)
} }

View File

@ -11,14 +11,13 @@ import (
) )
func Forums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func Forums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "forums", "forums")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ManageForums { if !user.Perms.ManageForums {
return common.NoPermissions(w, r, user) return common.NoPermissions(w, r, user)
} }
header.Title = common.GetTitlePhrase("panel_forums")
// TODO: Paginate this? // TODO: Paginate this?
var forumList []interface{} var forumList []interface{}
@ -39,14 +38,14 @@ func Forums(w http.ResponseWriter, r *http.Request, user common.User) common.Rou
} }
if r.FormValue("created") == "1" { if r.FormValue("created") == "1" {
header.AddNotice("panel_forum_created") basePage.AddNotice("panel_forum_created")
} else if r.FormValue("deleted") == "1" { } else if r.FormValue("deleted") == "1" {
header.AddNotice("panel_forum_deleted") basePage.AddNotice("panel_forum_deleted")
} else if r.FormValue("updated") == "1" { } else if r.FormValue("updated") == "1" {
header.AddNotice("panel_forum_updated") basePage.AddNotice("panel_forum_updated")
} }
pi := common.PanelPage{&common.BasePanelPage{header, stats, "forums", common.ReportForumID}, forumList, nil} pi := common.PanelPage{basePage, forumList, nil}
return panelRenderTemplate("panel_forums", w, r, user, &pi) return panelRenderTemplate("panel_forums", w, r, user, &pi)
} }
@ -76,14 +75,13 @@ func ForumsCreateSubmit(w http.ResponseWriter, r *http.Request, user common.User
// TODO: Revamp this // TODO: Revamp this
func ForumsDelete(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { func ForumsDelete(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "delete_forum", "forums")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ManageForums { if !user.Perms.ManageForums {
return common.NoPermissions(w, r, user) return common.NoPermissions(w, r, user)
} }
header.Title = common.GetTitlePhrase("panel_delete_forum")
fid, err := strconv.Atoi(sfid) fid, err := strconv.Atoi(sfid)
if err != nil { if err != nil {
@ -101,7 +99,7 @@ func ForumsDelete(w http.ResponseWriter, r *http.Request, user common.User, sfid
confirmMsg := "Are you sure you want to delete the '" + forum.Name + "' forum?" confirmMsg := "Are you sure you want to delete the '" + forum.Name + "' forum?"
yousure := common.AreYouSure{"/panel/forums/delete/submit/" + strconv.Itoa(fid), confirmMsg} yousure := common.AreYouSure{"/panel/forums/delete/submit/" + strconv.Itoa(fid), confirmMsg}
pi := common.PanelPage{&common.BasePanelPage{header, stats, "forums", common.ReportForumID}, tList, yousure} pi := common.PanelPage{basePage, tList, yousure}
if common.RunPreRenderHook("pre_render_panel_delete_forum", w, r, &user, &pi) { if common.RunPreRenderHook("pre_render_panel_delete_forum", w, r, &user, &pi) {
return nil return nil
} }
@ -138,14 +136,13 @@ func ForumsDeleteSubmit(w http.ResponseWriter, r *http.Request, user common.User
} }
func ForumsEdit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { func ForumsEdit(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "edit_forum", "forums")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ManageForums { if !user.Perms.ManageForums {
return common.NoPermissions(w, r, user) return common.NoPermissions(w, r, user)
} }
header.Title = common.GetTitlePhrase("panel_edit_forum")
fid, err := strconv.Atoi(sfid) fid, err := strconv.Atoi(sfid)
if err != nil { if err != nil {
@ -158,7 +155,6 @@ func ForumsEdit(w http.ResponseWriter, r *http.Request, user common.User, sfid s
} else if err != nil { } else if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
if forum.Preset == "" { if forum.Preset == "" {
forum.Preset = "custom" forum.Preset = "custom"
} }
@ -183,14 +179,14 @@ func ForumsEdit(w http.ResponseWriter, r *http.Request, user common.User, sfid s
} }
if r.FormValue("updated") == "1" { if r.FormValue("updated") == "1" {
header.AddNotice("panel_forum_updated") basePage.AddNotice("panel_forum_updated")
} }
pi := common.PanelEditForumPage{&common.BasePanelPage{header, stats, "forums", common.ReportForumID}, forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, gplist} pi := common.PanelEditForumPage{basePage, forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, gplist}
if common.RunPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) { if common.RunPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) {
return nil return nil
} }
err = common.Templates.ExecuteTemplate(w, "panel-forum-edit.html", pi) err = common.Templates.ExecuteTemplate(w, "panel_forum_edit.html", pi)
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
@ -297,14 +293,13 @@ func forumPermsExtractDash(paramList string) (fid int, gid int, err error) {
} }
func ForumsEditPermsAdvance(w http.ResponseWriter, r *http.Request, user common.User, paramList string) common.RouteError { func ForumsEditPermsAdvance(w http.ResponseWriter, r *http.Request, user common.User, paramList string) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "edit_forum", "forums")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ManageForums { if !user.Perms.ManageForums {
return common.NoPermissions(w, r, user) return common.NoPermissions(w, r, user)
} }
header.Title = common.GetTitlePhrase("panel_edit_forum")
fid, gid, err := forumPermsExtractDash(paramList) fid, gid, err := forumPermsExtractDash(paramList)
if err != nil { if err != nil {
@ -350,14 +345,14 @@ func ForumsEditPermsAdvance(w http.ResponseWriter, r *http.Request, user common.
addNameLangToggle("MoveTopic", forumPerms.MoveTopic) addNameLangToggle("MoveTopic", forumPerms.MoveTopic)
if r.FormValue("updated") == "1" { if r.FormValue("updated") == "1" {
header.AddNotice("panel_forums_perms_updated") basePage.AddNotice("panel_forums_perms_updated")
} }
pi := common.PanelEditForumGroupPage{&common.BasePanelPage{header, stats, "forums", common.ReportForumID}, forum.ID, gid, forum.Name, forum.Desc, forum.Active, forum.Preset, formattedPermList} pi := common.PanelEditForumGroupPage{basePage, forum.ID, gid, forum.Name, forum.Desc, forum.Active, forum.Preset, formattedPermList}
if common.RunPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) { if common.RunPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) {
return nil return nil
} }
err = common.Templates.ExecuteTemplate(w, "panel-forum-edit-perms.html", pi) err = common.Templates.ExecuteTemplate(w, "panel_forum_edit_perms.html", pi)
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }

View File

@ -11,11 +11,10 @@ import (
) )
func LogsRegs(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func LogsRegs(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "registration_logs", "logs")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_registration_logs")
logCount := common.RegLogs.GlobalCount() logCount := common.RegLogs.GlobalCount()
page, _ := strconv.Atoi(r.FormValue("page")) page, _ := strconv.Atoi(r.FormValue("page"))
@ -32,7 +31,7 @@ func LogsRegs(w http.ResponseWriter, r *http.Request, user common.User) common.R
} }
pageList := common.Paginate(logCount, perPage, 5) pageList := common.Paginate(logCount, perPage, 5)
pi := common.PanelRegLogsPage{&common.BasePanelPage{header, stats, "logs", common.ReportForumID}, llist, common.Paginator{pageList, page, lastPage}} pi := common.PanelRegLogsPage{basePage, llist, common.Paginator{pageList, page, lastPage}}
return panelRenderTemplate("panel_reglogs", w, r, user, &pi) return panelRenderTemplate("panel_reglogs", w, r, user, &pi)
} }
@ -102,11 +101,10 @@ func modlogsElementType(action string, elementType string, elementID int, actor
} }
func LogsMod(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func LogsMod(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "mod_logs", "logs")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_mod_logs")
logCount := common.ModLogs.GlobalCount() logCount := common.ModLogs.GlobalCount()
page, _ := strconv.Atoi(r.FormValue("page")) page, _ := strconv.Atoi(r.FormValue("page"))
@ -125,16 +123,15 @@ func LogsMod(w http.ResponseWriter, r *http.Request, user common.User) common.Ro
} }
pageList := common.Paginate(logCount, perPage, 5) pageList := common.Paginate(logCount, perPage, 5)
pi := common.PanelLogsPage{&common.BasePanelPage{header, stats, "logs", common.ReportForumID}, llist, common.Paginator{pageList, page, lastPage}} pi := common.PanelLogsPage{basePage, llist, common.Paginator{pageList, page, lastPage}}
return panelRenderTemplate("panel_modlogs", w, r, user, &pi) return panelRenderTemplate("panel_modlogs", w, r, user, &pi)
} }
func LogsAdmin(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func LogsAdmin(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "admin_logs", "logs")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_admin_logs")
logCount := common.ModLogs.GlobalCount() logCount := common.ModLogs.GlobalCount()
page, _ := strconv.Atoi(r.FormValue("page")) page, _ := strconv.Atoi(r.FormValue("page"))
@ -153,6 +150,6 @@ func LogsAdmin(w http.ResponseWriter, r *http.Request, user common.User) common.
} }
pageList := common.Paginate(logCount, perPage, 5) pageList := common.Paginate(logCount, perPage, 5)
pi := common.PanelLogsPage{&common.BasePanelPage{header, stats, "logs", common.ReportForumID}, llist, common.Paginator{pageList, page, lastPage}} pi := common.PanelLogsPage{basePage, llist, common.Paginator{pageList, page, lastPage}}
return panelRenderTemplate("panel_adminlogs", w, r, user, &pi) return panelRenderTemplate("panel_adminlogs", w, r, user, &pi)
} }

View File

@ -9,16 +9,15 @@ import (
) )
func Pages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func Pages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "pages", "pages")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_pages")
if r.FormValue("created") == "1" { if r.FormValue("created") == "1" {
header.AddNotice("panel_page_created") basePage.AddNotice("panel_page_created")
} else if r.FormValue("deleted") == "1" { } else if r.FormValue("deleted") == "1" {
header.AddNotice("panel_page_deleted") basePage.AddNotice("panel_page_deleted")
} }
pageCount := common.Pages.GlobalCount() pageCount := common.Pages.GlobalCount()
@ -32,7 +31,7 @@ func Pages(w http.ResponseWriter, r *http.Request, user common.User) common.Rout
} }
pageList := common.Paginate(pageCount, perPage, 5) pageList := common.Paginate(pageCount, perPage, 5)
pi := common.PanelCustomPagesPage{&common.BasePanelPage{header, stats, "pages", common.ReportForumID}, cPages, common.Paginator{pageList, page, lastPage}} pi := common.PanelCustomPagesPage{basePage, cPages, common.Paginator{pageList, page, lastPage}}
return panelRenderTemplate("panel_pages", w, r, user, &pi) return panelRenderTemplate("panel_pages", w, r, user, &pi)
} }
@ -69,14 +68,12 @@ func PagesCreateSubmit(w http.ResponseWriter, r *http.Request, user common.User)
} }
func PagesEdit(w http.ResponseWriter, r *http.Request, user common.User, spid string) common.RouteError { func PagesEdit(w http.ResponseWriter, r *http.Request, user common.User, spid string) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "pages_edit", "pages")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
header.Title = common.GetTitlePhrase("panel_pages_edit")
if r.FormValue("updated") == "1" { if r.FormValue("updated") == "1" {
header.AddNotice("panel_page_updated") basePage.AddNotice("panel_page_updated")
} }
pid, err := strconv.Atoi(spid) pid, err := strconv.Atoi(spid)
@ -86,12 +83,12 @@ func PagesEdit(w http.ResponseWriter, r *http.Request, user common.User, spid st
page, err := common.Pages.Get(pid) page, err := common.Pages.Get(pid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return common.NotFound(w, r, header) return common.NotFound(w, r, basePage.Header)
} else if err != nil { } else if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
pi := common.PanelCustomPageEditPage{&common.BasePanelPage{header, stats, "pages", common.ReportForumID}, page} pi := common.PanelCustomPageEditPage{basePage, page}
return panelRenderTemplate("panel_pages_edit", w, r, user, &pi) return panelRenderTemplate("panel_pages_edit", w, r, user, &pi)
} }

View File

@ -11,16 +11,15 @@ import (
) )
func Settings(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func Settings(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "settings", "settings")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.EditSettings { if !user.Perms.EditSettings {
return common.NoPermissions(w, r, user) return common.NoPermissions(w, r, user)
} }
header.Title = common.GetTitlePhrase("panel_settings")
settings, err := header.Settings.BypassGetAll() settings, err := basePage.Settings.BypassGetAll()
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
@ -49,21 +48,20 @@ func Settings(w http.ResponseWriter, r *http.Request, user common.User) common.R
settingList = append(settingList, &common.PanelSetting{setting, common.GetSettingPhrase(setting.Name)}) settingList = append(settingList, &common.PanelSetting{setting, common.GetSettingPhrase(setting.Name)})
} }
pi := common.PanelPage{&common.BasePanelPage{header, stats, "settings", common.ReportForumID}, tList, settingList} pi := common.PanelPage{basePage, tList, settingList}
return panelRenderTemplate("panel_settings", w, r, user, &pi) return panelRenderTemplate("panel_settings", w, r, user, &pi)
} }
func SettingEdit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError { func SettingEdit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError {
header, stats, ferr := common.PanelUserCheck(w, r, &user) basePage, ferr := buildBasePage(w, r, &user, "edit_setting", "settings")
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.EditSettings { if !user.Perms.EditSettings {
return common.NoPermissions(w, r, user) return common.NoPermissions(w, r, user)
} }
header.Title = common.GetTitlePhrase("panel_edit_setting")
setting, err := header.Settings.BypassGet(sname) setting, err := basePage.Settings.BypassGet(sname)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return common.LocalError("The setting you want to edit doesn't exist.", w, r, user) return common.LocalError("The setting you want to edit doesn't exist.", w, r, user)
} else if err != nil { } else if err != nil {
@ -91,7 +89,7 @@ func SettingEdit(w http.ResponseWriter, r *http.Request, user common.User, sname
} }
pSetting := &common.PanelSetting{setting, common.GetSettingPhrase(setting.Name)} pSetting := &common.PanelSetting{setting, common.GetSettingPhrase(setting.Name)}
pi := common.PanelSettingPage{&common.BasePanelPage{header, stats, "settings", common.ReportForumID}, itemList, pSetting} pi := common.PanelSettingPage{basePage, itemList, pSetting}
return panelRenderTemplate("panel_setting", w, r, user, &pi) return panelRenderTemplate("panel_setting", w, r, user, &pi)
} }

171
routes/panel/users.go Normal file
View File

@ -0,0 +1,171 @@
package panel
import (
"database/sql"
"net/http"
"strconv"
"../../common"
)
func Users(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "users", "users")
if ferr != nil {
return ferr
}
page, _ := strconv.Atoi(r.FormValue("page"))
perPage := 10
offset, page, lastPage := common.PageOffset(basePage.Stats.Users, page, perPage)
users, err := common.Users.GetOffset(offset, perPage)
if err != nil {
return common.InternalError(err, w, r)
}
pageList := common.Paginate(basePage.Stats.Users, perPage, 5)
pi := common.PanelUserPage{basePage, users, common.Paginator{pageList, page, lastPage}}
return panelRenderTemplate("panel_users", w, r, user, &pi)
}
func UsersEdit(w http.ResponseWriter, r *http.Request, user common.User, suid string) common.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "edit_user", "users")
if ferr != nil {
return ferr
}
if !user.Perms.EditUser {
return common.NoPermissions(w, r, user)
}
uid, err := strconv.Atoi(suid)
if err != nil {
return common.LocalError("The provided UserID is not a valid number.", w, r, user)
}
targetUser, err := common.Users.Get(uid)
if err == sql.ErrNoRows {
return common.LocalError("The user you're trying to edit doesn't exist.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
if targetUser.IsAdmin && !user.IsAdmin {
return common.LocalError("Only administrators can edit the account of an administrator.", w, r, user)
}
// ? - Should we stop admins from deleting all the groups? Maybe, protect the group they're currently using?
groups, err := common.Groups.GetRange(1, 0) // ? - 0 = Go to the end
if err != nil {
return common.InternalError(err, w, r)
}
var groupList []interface{}
for _, group := range groups {
if !user.Perms.EditUserGroupAdmin && group.IsAdmin {
continue
}
if !user.Perms.EditUserGroupSuperMod && group.IsMod {
continue
}
groupList = append(groupList, group)
}
if r.FormValue("updated") == "1" {
basePage.AddNotice("panel_user_updated")
}
pi := common.PanelPage{basePage, groupList, targetUser}
if common.RunPreRenderHook("pre_render_panel_edit_user", w, r, &user, &pi) {
return nil
}
err = common.Templates.ExecuteTemplate(w, "panel_user_edit.html", pi)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func UsersEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, suid string) common.RouteError {
_, ferr := common.SimplePanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
if !user.Perms.EditUser {
return common.NoPermissions(w, r, user)
}
uid, err := strconv.Atoi(suid)
if err != nil {
return common.LocalError("The provided UserID is not a valid number.", w, r, user)
}
targetUser, err := common.Users.Get(uid)
if err == sql.ErrNoRows {
return common.LocalError("The user you're trying to edit doesn't exist.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
if targetUser.IsAdmin && !user.IsAdmin {
return common.LocalError("Only administrators can edit the account of other administrators.", w, r, user)
}
newname := common.SanitiseSingleLine(r.PostFormValue("user-name"))
if newname == "" {
return common.LocalError("You didn't put in a username.", w, r, user)
}
// TODO: How should activation factor into admin set emails?
// TODO: How should we handle secondary emails? Do we even have secondary emails implemented?
newemail := common.SanitiseSingleLine(r.PostFormValue("user-email"))
if newemail == "" {
return common.LocalError("You didn't put in an email address.", w, r, user)
}
if (newemail != targetUser.Email) && !user.Perms.EditUserEmail {
return common.LocalError("You need the EditUserEmail permission to edit the email address of a user.", w, r, user)
}
newpassword := r.PostFormValue("user-password")
if newpassword != "" && !user.Perms.EditUserPassword {
return common.LocalError("You need the EditUserPassword permission to edit the password of a user.", w, r, user)
}
newgroup, err := strconv.Atoi(r.PostFormValue("user-group"))
if err != nil {
return common.LocalError("You need to provide a whole number for the group ID", w, r, user)
}
group, err := common.Groups.Get(newgroup)
if err == sql.ErrNoRows {
return common.LocalError("The group you're trying to place this user in doesn't exist.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
if !user.Perms.EditUserGroupAdmin && group.IsAdmin {
return common.LocalError("You need the EditUserGroupAdmin permission to assign someone to an administrator group.", w, r, user)
}
if !user.Perms.EditUserGroupSuperMod && group.IsMod {
return common.LocalError("You need the EditUserGroupSuperMod permission to assign someone to a super mod group.", w, r, user)
}
err = targetUser.Update(newname, newemail, newgroup)
if err != nil {
return common.InternalError(err, w, r)
}
if newpassword != "" {
common.SetPassword(targetUser.ID, newpassword)
// Log the user out as a safety precaution
common.Auth.ForceLogout(targetUser.ID)
}
targetUser.CacheRemove()
// If we're changing our own password, redirect to the index rather than to a noperms error due to the force logout
if targetUser.ID == user.ID {
http.Redirect(w, r, "/", http.StatusSeeOther)
} else {
http.Redirect(w, r, "/panel/users/edit/"+strconv.Itoa(targetUser.ID)+"?updated=1", http.StatusSeeOther)
}
return nil
}

View File

@ -31,7 +31,7 @@ INSERT INTO [menus] () VALUES ();
INSERT INTO [menu_items] ([mid],[name],[htmlID],[position],[path],[aria],[tooltip],[order]) VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0); INSERT INTO [menu_items] ([mid],[name],[htmlID],[position],[path],[aria],[tooltip],[order]) VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0);
INSERT INTO [menu_items] ([mid],[name],[htmlID],[cssClass],[position],[path],[aria],[tooltip],[order]) VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1); INSERT INTO [menu_items] ([mid],[name],[htmlID],[cssClass],[position],[path],[aria],[tooltip],[order]) VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1);
INSERT INTO [menu_items] ([mid],[htmlID],[cssClass],[position],[tmplName],[order]) VALUES (1,'general_alerts','menu_alerts','right','menu_alerts',2); INSERT INTO [menu_items] ([mid],[htmlID],[cssClass],[position],[tmplName],[order]) VALUES (1,'general_alerts','menu_alerts','right','menu_alerts',2);
INSERT INTO [menu_items] ([mid],[name],[cssClass],[position],[path],[aria],[tooltip],[memberOnly],[order]) VALUES (1,'{lang.menu_account}','menu_account','left','/user/edit/critical/','{lang.menu_account_aria}','{lang.menu_account_tooltip}',1,3); INSERT INTO [menu_items] ([mid],[name],[cssClass],[position],[path],[aria],[tooltip],[memberOnly],[order]) VALUES (1,'{lang.menu_account}','menu_account','left','/user/edit/','{lang.menu_account_aria}','{lang.menu_account_tooltip}',1,3);
INSERT INTO [menu_items] ([mid],[name],[cssClass],[position],[path],[aria],[tooltip],[memberOnly],[order]) VALUES (1,'{lang.menu_profile}','menu_profile','left','{me.Link}','{lang.menu_profile_aria}','{lang.menu_profile_tooltip}',1,4); INSERT INTO [menu_items] ([mid],[name],[cssClass],[position],[path],[aria],[tooltip],[memberOnly],[order]) VALUES (1,'{lang.menu_profile}','menu_profile','left','{me.Link}','{lang.menu_profile_aria}','{lang.menu_profile_tooltip}',1,4);
INSERT INTO [menu_items] ([mid],[name],[cssClass],[position],[path],[aria],[tooltip],[memberOnly],[staffOnly],[order]) VALUES (1,'{lang.menu_panel}','menu_panel menu_account','left','/panel/','{lang.menu_panel_aria}','{lang.menu_panel_tooltip}',1,1,5); INSERT INTO [menu_items] ([mid],[name],[cssClass],[position],[path],[aria],[tooltip],[memberOnly],[staffOnly],[order]) VALUES (1,'{lang.menu_panel}','menu_panel menu_account','left','/panel/','{lang.menu_panel_aria}','{lang.menu_panel_tooltip}',1,1,5);
INSERT INTO [menu_items] ([mid],[name],[cssClass],[position],[path],[aria],[tooltip],[memberOnly],[order]) VALUES (1,'{lang.menu_logout}','menu_logout','left','/accounts/logout/?session={me.Session}','{lang.menu_logout_aria}','{lang.menu_logout_tooltip}',1,6); INSERT INTO [menu_items] ([mid],[name],[cssClass],[position],[path],[aria],[tooltip],[memberOnly],[order]) VALUES (1,'{lang.menu_logout}','menu_logout','left','/accounts/logout/?session={me.Session}','{lang.menu_logout_aria}','{lang.menu_logout_tooltip}',1,6);

View File

@ -0,0 +1,14 @@
CREATE TABLE [users_2fa_keys] (
[uid] int not null,
[secret] nvarchar (100) not null,
[scratch1] nvarchar (50) not null,
[scratch2] nvarchar (50) not null,
[scratch3] nvarchar (50) not null,
[scratch4] nvarchar (50) not null,
[scratch5] nvarchar (50) not null,
[scratch6] nvarchar (50) not null,
[scratch7] nvarchar (50) not null,
[scratch8] nvarchar (50) not null,
[createdAt] datetime not null,
primary key([uid])
);

View File

@ -31,7 +31,7 @@ INSERT INTO `menus`() VALUES ();
INSERT INTO `menu_items`(`mid`,`name`,`htmlID`,`position`,`path`,`aria`,`tooltip`,`order`) VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0); INSERT INTO `menu_items`(`mid`,`name`,`htmlID`,`position`,`path`,`aria`,`tooltip`,`order`) VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0);
INSERT INTO `menu_items`(`mid`,`name`,`htmlID`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`order`) VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1); INSERT INTO `menu_items`(`mid`,`name`,`htmlID`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`order`) VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1);
INSERT INTO `menu_items`(`mid`,`htmlID`,`cssClass`,`position`,`tmplName`,`order`) VALUES (1,'general_alerts','menu_alerts','right','menu_alerts',2); INSERT INTO `menu_items`(`mid`,`htmlID`,`cssClass`,`position`,`tmplName`,`order`) VALUES (1,'general_alerts','menu_alerts','right','menu_alerts',2);
INSERT INTO `menu_items`(`mid`,`name`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`memberOnly`,`order`) VALUES (1,'{lang.menu_account}','menu_account','left','/user/edit/critical/','{lang.menu_account_aria}','{lang.menu_account_tooltip}',1,3); INSERT INTO `menu_items`(`mid`,`name`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`memberOnly`,`order`) VALUES (1,'{lang.menu_account}','menu_account','left','/user/edit/','{lang.menu_account_aria}','{lang.menu_account_tooltip}',1,3);
INSERT INTO `menu_items`(`mid`,`name`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`memberOnly`,`order`) VALUES (1,'{lang.menu_profile}','menu_profile','left','{me.Link}','{lang.menu_profile_aria}','{lang.menu_profile_tooltip}',1,4); INSERT INTO `menu_items`(`mid`,`name`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`memberOnly`,`order`) VALUES (1,'{lang.menu_profile}','menu_profile','left','{me.Link}','{lang.menu_profile_aria}','{lang.menu_profile_tooltip}',1,4);
INSERT INTO `menu_items`(`mid`,`name`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`memberOnly`,`staffOnly`,`order`) VALUES (1,'{lang.menu_panel}','menu_panel menu_account','left','/panel/','{lang.menu_panel_aria}','{lang.menu_panel_tooltip}',1,1,5); INSERT INTO `menu_items`(`mid`,`name`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`memberOnly`,`staffOnly`,`order`) VALUES (1,'{lang.menu_panel}','menu_panel menu_account','left','/panel/','{lang.menu_panel_aria}','{lang.menu_panel_tooltip}',1,1,5);
INSERT INTO `menu_items`(`mid`,`name`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`memberOnly`,`order`) VALUES (1,'{lang.menu_logout}','menu_logout','left','/accounts/logout/?session={me.Session}','{lang.menu_logout_aria}','{lang.menu_logout_tooltip}',1,6); INSERT INTO `menu_items`(`mid`,`name`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`memberOnly`,`order`) VALUES (1,'{lang.menu_logout}','menu_logout','left','/accounts/logout/?session={me.Session}','{lang.menu_logout_aria}','{lang.menu_logout_tooltip}',1,6);

View File

@ -0,0 +1,14 @@
CREATE TABLE `users_2fa_keys` (
`uid` int not null,
`secret` varchar(100) not null,
`scratch1` varchar(50) not null,
`scratch2` varchar(50) not null,
`scratch3` varchar(50) not null,
`scratch4` varchar(50) not null,
`scratch5` varchar(50) not null,
`scratch6` varchar(50) not null,
`scratch7` varchar(50) not null,
`scratch8` varchar(50) not null,
`createdAt` datetime not null,
primary key(`uid`)
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;

View File

@ -31,7 +31,7 @@ INSERT INTO "menus"() VALUES ();
INSERT INTO "menu_items"("mid","name","htmlID","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0); INSERT INTO "menu_items"("mid","name","htmlID","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0);
INSERT INTO "menu_items"("mid","name","htmlID","cssClass","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1); INSERT INTO "menu_items"("mid","name","htmlID","cssClass","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1);
INSERT INTO "menu_items"("mid","htmlID","cssClass","position","tmplName","order") VALUES (1,'general_alerts','menu_alerts','right','menu_alerts',2); INSERT INTO "menu_items"("mid","htmlID","cssClass","position","tmplName","order") VALUES (1,'general_alerts','menu_alerts','right','menu_alerts',2);
INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","order") VALUES (1,'{lang.menu_account}','menu_account','left','/user/edit/critical/','{lang.menu_account_aria}','{lang.menu_account_tooltip}',1,3); INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","order") VALUES (1,'{lang.menu_account}','menu_account','left','/user/edit/','{lang.menu_account_aria}','{lang.menu_account_tooltip}',1,3);
INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","order") VALUES (1,'{lang.menu_profile}','menu_profile','left','{me.Link}','{lang.menu_profile_aria}','{lang.menu_profile_tooltip}',1,4); INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","order") VALUES (1,'{lang.menu_profile}','menu_profile','left','{me.Link}','{lang.menu_profile_aria}','{lang.menu_profile_tooltip}',1,4);
INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","staffOnly","order") VALUES (1,'{lang.menu_panel}','menu_panel menu_account','left','/panel/','{lang.menu_panel_aria}','{lang.menu_panel_tooltip}',1,1,5); INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","staffOnly","order") VALUES (1,'{lang.menu_panel}','menu_panel menu_account','left','/panel/','{lang.menu_panel_aria}','{lang.menu_panel_tooltip}',1,1,5);
INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","order") VALUES (1,'{lang.menu_logout}','menu_logout','left','/accounts/logout/?session={me.Session}','{lang.menu_logout_aria}','{lang.menu_logout_tooltip}',1,6); INSERT INTO "menu_items"("mid","name","cssClass","position","path","aria","tooltip","memberOnly","order") VALUES (1,'{lang.menu_logout}','menu_logout','left','/accounts/logout/?session={me.Session}','{lang.menu_logout_aria}','{lang.menu_logout_tooltip}',1,6);

View File

@ -0,0 +1,14 @@
CREATE TABLE `users_2fa_keys` (
`uid` int not null,
`secret` varchar (100) not null,
`scratch1` varchar (50) not null,
`scratch2` varchar (50) not null,
`scratch3` varchar (50) not null,
`scratch4` varchar (50) not null,
`scratch5` varchar (50) not null,
`scratch6` varchar (50) not null,
`scratch7` varchar (50) not null,
`scratch8` varchar (50) not null,
`createdAt` timestamp not null,
primary key(`uid`)
);

View File

@ -1,5 +1,5 @@
{ {
"DBVersion":"5", "DBVersion":"6",
"DynamicFileVersion":"0", "DynamicFileVersion":"0",
"MinGoVersion":"1.10", "MinGoVersion":"1.10",
"MinVersion":"" "MinVersion":""

View File

@ -1,15 +1,13 @@
<nav class="colstack_left"> <nav class="colstack_left">
<div class="colstack_item colstack_head rowhead menuhead"> <div class="colstack_item colstack_head rowhead menuhead">
<div class="rowitem"> <div class="rowitem">
<a href="/user/edit/critical/"><h1>{{lang "account_menu_head"}}</h1></a> <a href="/user/edit/"><h1>{{lang "account_menu_head"}}</h1></a>
</div> </div>
</div> </div>
<div class="colstack_item rowmenu"> <div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/user/edit/avatar/">{{lang "account_menu_avatar"}}</a></div> <div class="rowitem passive"><a href="/user/edit/password/">{{lang "account_menu_password"}}</a></div>
<div class="rowitem passive"><a href="/user/edit/username/">{{lang "account_menu_username"}}</a></div>
<div class="rowitem passive"><a href="/user/edit/critical/">{{lang "account_menu_password"}}</a></div>
<div class="rowitem passive"><a href="/user/edit/email/">{{lang "account_menu_email"}}</a></div> <div class="rowitem passive"><a href="/user/edit/email/">{{lang "account_menu_email"}}</a></div>
<!--<div class="rowitem passive"><a href="/user/edit/notifications/">{{lang "account_menu_notifications"}}</a></div>--> <div class="rowitem passive"><a href="/user/edit/notifications/">{{lang "account_menu_notifications"}}</a> <span class="account_soon">Coming Soon</span></div>
{{/** 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 **/}} {{/** 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 **/}}
</div> </div>
</nav> </nav>

View File

@ -0,0 +1,31 @@
{{template "header.html" . }}
<div id="account_dashboard" class="colstack account">
{{template "account_menu.html" . }}
<main class="colstack_right">
<form id="avatar_form" action="/user/edit/avatar/submit/?session={{.CurrentUser.Session}}" method="post" enctype="multipart/form-data"></form>
<div class="coldyn_block">
<div id="dash_left" class="coldyn_item">
<div class="rowitem">
<span id="dash_saved">Saved</span>
<!--<span id="dash_username">{{.CurrentUser.Name}}</span>-->
<span id="dash_username">
<form id="dash_username_form" action="/user/edit/username/submit/?session={{.CurrentUser.Session}}" method="post"></form>
<input form="dash_username_form" name="account-new-username" value="{{.CurrentUser.Name}}" />
<button form="dash_username_form" class="formbutton">Save</button>
</span>
<img src="{{.CurrentUser.Avatar}}" height="128px" />
<span id="dash_avatar_buttons">
<input form="avatar_form" id="select_avatar" name="account-avatar" type="file" required style="display: none;" />
<label for="select_avatar" class="formbutton">Select</label>
<button form="avatar_form" name="account-button" class="formbutton">{{lang "account_avatar_update_button"}}</button>
</span>
</div>
</div>
<div id="dash_right" class="coldyn_item">
<div class="rowitem">{{if not .MFASetup}}<a href="/user/edit/mfa/setup/">{{lang "account_dash_2fa_setup"}}</a>{{else}}<a href="/user/edit/mfa/">{{lang "account_dash_2fa_manage"}}</a>{{end}} <span class="dash_security">{{lang "account_dash_security_notice"}}</span></div>
<div class="rowitem">{{lang "account_dash_next_level"}} <span class="account_soon">{{lang "account_coming_soon"}}</span></div>
</div>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,24 +0,0 @@
{{template "header.html" . }}
<div id="account_edit_avatar" class="colstack account">
{{template "account_menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head rowhead">
<div class="rowitem"><h1>{{lang "account_avatar_head"}}</h1></div>
</div>
<div class="colstack_item avatar_box">
<div class="rowitem"><img src="{{.CurrentUser.Avatar}}" height="128px" max-width="128px" /></div>
</div>
<div class="colstack_item the_form">
<form action="/user/edit/avatar/submit/?session={{.CurrentUser.Session}}" method="post" enctype="multipart/form-data">
<div class="formrow real_first_child">
<div class="formitem formlabel"><a>{{lang "account_avatar_upload_label"}}</a></div>
<div class="formitem"><input name="account-avatar" type="file" required /></div>
</div>
<div class="formrow">
<div class="formitem"><button name="account-button" class="formbutton">{{lang "account_avatar_update_button"}}</button></div>
</div>
</form>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -0,0 +1,29 @@
{{template "header.html" . }}
<div id="account_edit_mfa" class="colstack account">
{{template "account_menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head rowhead">
<div class="rowitem"><h1>{{lang "account_mfa_head"}}</h1></div>
</div>
<div class="colstack_item the_form">
<form action="/user/edit/mfa/disable/submit/?session={{.CurrentUser.Session}}" method="post">
<div class="formrow real_first_child">
<div class="formitem formlabel"><a>{{lang "account_mfa_disable_explanation"}}</a></div>
<div class="formitem"><button class="formbutton">{{lang "account_mfa_disable_button"}}</button></div>
</div>
</form>
</div>
<div class="colstack_item colstack_head rowhead">
<div class="rowitem"><h1>{{lang "account_mfa_scratch_head"}}</h1></div>
</div>
<div class="colstack_item">{{/** TODO: Don't inline this, figure a way of implementing it properly in the template system **/}}
<div class="rowitem rowmsg" style="white-space: pre-wrap;">{{lang "account_mfa_scratch_explanation"}}</div>
</div>
<div id="panel_mfa_scratches" class="colstack_item rowlist">
{{range .Something}}
<div class="rowitem">{{.}}</div>
{{end}}
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -0,0 +1,26 @@
{{template "header.html" . }}
<div id="account_edit_mfa_setup" class="colstack account">
{{template "account_menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head rowhead">
<div class="rowitem"><h1>{{lang "account_mfa_setup_head"}}</h1></div>
</div>
<div class="colstack_item the_form">
<form action="/user/edit/mfa/setup/submit/?session={{.CurrentUser.Session}}" method="post">
<input name="code" value="{{.Something}}" type="hidden" />
<div class="formrow real_first_child">
<div class="formitem formlabel"><a>{{lang "account_mfa_setup_explanation"}}</a></div>
<div class="formitem formlabel">{{.Something}}</div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "account_mfa_setup_verify"}}</a></div>
<div class="formitem"><input name="otp" type="text" autocomplete="off" required /></div> {{/** TODO: Make this a password? **/}}
</div>
<div class="formrow">
<div class="formitem"><button name="account-button" class="formbutton form_middle_button">{{lang "account_mfa_setup_button"}}</button></div>
</div>
</form>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -6,7 +6,7 @@
<div class="rowitem"><h1>{{lang "account_password_head"}}</h1></div> <div class="rowitem"><h1>{{lang "account_password_head"}}</h1></div>
</div> </div>
<div class="colstack_item the_form"> <div class="colstack_item the_form">
<form action="/user/edit/critical/submit/?session={{.CurrentUser.Session}}" method="post"> <form action="/user/edit/password/submit/?session={{.CurrentUser.Session}}" method="post">
<div class="formrow real_first_child"> <div class="formrow real_first_child">
<div class="formitem formlabel"><a>{{lang "account_password_current_password"}}</a></div> <div class="formitem formlabel"><a>{{lang "account_password_current_password"}}</a></div>
<div class="formitem"><input name="account-current-password" type="password" placeholder="*****" autocomplete="current-password" required /></div> <div class="formitem"><input name="account-current-password" type="password" placeholder="*****" autocomplete="current-password" required /></div>

View File

@ -1,25 +0,0 @@
{{template "header.html" . }}
<div id="account_edit_username" class="colstack account">
{{template "account_menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head rowhead">
<div class="rowitem"><h1>{{lang "account_username_head"}}</h1></div>
</div>
<div class="colstack_item the_form">
<form action="/user/edit/username/submit/?session={{.CurrentUser.Session}}" method="post">
<div class="formrow real_first_child">
<div class="formitem formlabel"><a>{{lang "account_username_current_username"}}</a></div>
<div class="formitem formlabel">{{.CurrentUser.Name}}</div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "account_username_new_username"}}</a></div>
<div class="formitem"><input name="account-new-username" type="text" required /></div>
</div>
<div class="formrow">
<div class="formitem"><button name="account-button" class="formbutton form_middle_button">{{lang "account_username_update_button"}}</button></div>
</div>
</form>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -14,10 +14,13 @@
<script type="text/javascript"> <script type="text/javascript">
var session = "{{.CurrentUser.Session}}"; var session = "{{.CurrentUser.Session}}";
var siteURL = "{{.Header.Site.URL}}"; var siteURL = "{{.Header.Site.URL}}";
var maxRequestSize = "{{.Header.Site.MaxRequestSize}}";
</script> </script>
<script type="text/javascript" src="/static/global.js"></script> <script type="text/javascript" src="/static/global.js"></script>
<meta name="viewport" content="width=device-width,initial-scale = 1.0, maximum-scale=1.0,user-scalable=no" /> <meta name="viewport" content="width=device-width,initial-scale = 1.0, maximum-scale=1.0,user-scalable=no" />
{{if .Header.MetaDesc}}<meta name="description" content="{{.Header.MetaDesc}}" />{{end}} {{if .Header.MetaDesc}}<meta name="description" content="{{.Header.MetaDesc}}" />{{end}}
<meta property="og:site_name" content="{{.Header.Site.Name}}">
<meta property="og:title" content="{{.Title}} | {{.Header.Site.Name}}">
</head> </head>
<body> <body>
{{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}} {{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}}
@ -53,6 +56,6 @@
<div class="midLeft"></div> <div class="midLeft"></div>
<div id="back" class="zone_{{.Header.Zone}}{{if .Header.Widgets.RightSidebar}} shrink_main{{end}}"> <div id="back" class="zone_{{.Header.Zone}}{{if .Header.Widgets.RightSidebar}} shrink_main{{end}}">
<div id="main" > <div id="main" >
<div class="alertbox">{{range .Header.NoticeList}} <div class="alertbox initial_alertbox">{{range .Header.NoticeList}}
{{template "notice.html" . }}{{end}} {{template "notice.html" . }}{{end}}
</div> </div>

View File

@ -0,0 +1,21 @@
{{template "header.html" . }}
<main id="login_page">
<div class="rowblock rowhead">
<div class="rowitem"><h1>{{lang "login_mfa_verify_head"}}</h1></div>
</div>
<div class="rowblock">
<form action="/accounts/mfa_verify/submit/" method="post">
<div class="formrow real_first_child">
<div class="formitem formlabel"><a>{{lang "login_mfa_verify_explanation"}}</a></div>
</div>
<div class="formrow login_mfa_token_row">
<div class="formitem formlabel"><a id="login_mfa_verify_label">{{lang "login_mfa_token"}}</a></div>
<div class="formitem"><input name="mfa_token" type="text" autocomplete="off" placeholder="*****" aria-labelledby="login_mfa_verify_label" required /></div>
</div>
<div class="formrow login_button_row">
<div class="formitem"><button name="login-button" class="formbutton">{{lang "login_mfa_verify_button"}}</button></div>
</div>
</form>
</div>
</main>
{{template "footer.html" . }}

View File

@ -0,0 +1,88 @@
.sidebar, .footer .widget {
display: none;
}
#account_dashboard .colstack_right .coldyn_block {
display: flex;
}
#account_dashboard .coldyn_item {
margin-left: 16px;
}
#dash_left {
border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color);
background-color: var(--element-background-color);
padding: 18px;
height: 184px;
position: relative;
}
#dash_saved {
text-transform: uppercase;
font-size: 11px;
color: green;
position: absolute;
right: 8px;
top: 8px;
display: none;
}
.dash_security, .account_soon {
text-transform: uppercase;
font-size: 11px;
color: maroon;
}
#dash_username {
display: flex;
font-size: 18px;
text-align: center;
margin-bottom: 6px;
}
#dash_username input {
font-size: 16px;
width: 130px;
width: 80px;
padding-left: 8px;
margin-top: -4px;
margin-bottom: 6px;
margin-left: auto;
margin-right: auto;
color: hsl(0,0%,45%); /* TODO: Use this colour elsewhere? */
text-align: center;
}
#dash_username button {
display: none;
margin-left: 4px;
padding: 6px;
margin-top: 0px;
margin-bottom: 6px;
padding-top: 4px;
padding-bottom: 4px;
}
#dash_left img {
display: block;
border-radius: 48px;
height: 72px;
width: 72px;
margin-left: auto;
margin-right: auto;
}
#dash_left label {
display: inline-block;
margin-right: 8px;
}
#dash_avatar_buttons {
display: flex;
margin-bottom: 3px;
}
#dash_right {
width: 100%;
background: none !important;
border: none !important;
}
#dash_right .rowitem {
border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color);
background-color: var(--element-background-color);
padding: 16px;
}
#dash_right .rowitem:not(:last-child) {
margin-bottom: 8px;
}

View File

@ -1245,7 +1245,7 @@ textarea {
border-top: 1px solid var(--element-border-color) !important; border-top: 1px solid var(--element-border-color) !important;
} }
.colstack_item .formrow { /*.colstack_item .formrow {
display: flex; display: flex;
} }
.colstack_right .formrow { .colstack_right .formrow {
@ -1267,7 +1267,7 @@ textarea {
width: 40%; width: 40%;
margin-right: 12px; margin-right: 12px;
white-space: nowrap; white-space: nowrap;
} }*/
.formitem:only-child { .formitem:only-child {
width: 100%; width: 100%;
display: flex; display: flex;
@ -1295,25 +1295,28 @@ textarea {
#create_topic_page .close_form, #create_topic_page .formlabel, #login_page .formlabel { #create_topic_page .close_form, #create_topic_page .formlabel, #login_page .formlabel {
display: none; display: none;
} }
#login_page .formrow:not(:first-child):not(:last-child), #register_page .formrow:not(:first-child):not(:last-child) { .formrow:not(:first-child):not(:last-child) {
margin-top: 4px; margin-top: 4px;
} }
#login_page .formrow:not(:first-child), #register_page .formrow:not(:first-child) { .formrow:not(:first-child) {
padding-top: 3px; padding-top: 3px;
} }
.formrow {
padding: 16px;
}
.formrow:not(:last-child) {
padding-bottom: 4px;
}
#login_page .formrow:not(:last-child) { #login_page .formrow:not(:last-child) {
padding-bottom: 0px; padding-bottom: 0px;
} }
#login_page .formrow, #register_page .formrow { .formlabel {
padding: 16px;
}
#register_page .formrow:not(:last-child) {
padding-bottom: 4px;
}
#register_page .formlabel {
display: block; display: block;
font-size: 15px; font-size: 15px;
} }
.quick_create_form .formrow {
padding: 0px;
}
#register_page .register_button_row { #register_page .register_button_row {
padding: 12px !important; padding: 12px !important;
padding-top: 0px !important; padding-top: 0px !important;

View File

@ -90,8 +90,14 @@ $(document).ready(function(){
// Move the alerts under the first header // Move the alerts under the first header
let colSel = $(".colstack_right .colstack_head:first"); let colSel = $(".colstack_right .colstack_head:first");
let colSelAlt = $(".colstack_right .colstack_item:first");
let colSelAltAlt = $(".colstack_right .coldyn_block:first");
if(colSel.length > 0) { if(colSel.length > 0) {
$('.alert').insertAfter(colSel); $('.alert').insertAfter(colSel);
} else if (colSelAlt.length > 0) {
$('.alert').insertBefore(colSelAlt);
} else if (colSelAltAlt.length > 0) {
$('.alert').insertBefore(colSelAltAlt);
} else { } else {
$('.alert').insertAfter(".rowhead:first"); $('.alert').insertAfter(".rowhead:first");
} }

View File

@ -0,0 +1,69 @@
.sidebar, .footer .widget {
display: none;
}
#account_dashboard .colstack_right .coldyn_block {
display: flex;
}
#dash_left {
border-radius: 3px;
background-color: #444444;
padding: 12px;
height: 180px;
width: 240px;
position: relative;
}
#dash_saved {
display: none;
}
#dash_username {
display: flex;
}
#dash_username input {
display: block;
margin-left: auto;
margin-right: auto;
margin-bottom: 8px;
/*font-size: 16px;*/
width: 100px;
display: relative;
padding-left: 16px;
background-position: right 8px bottom 8px;
}
#dash_username button {
margin-bottom: 8px;
padding-top: 2px;
padding-bottom: 2px;
}
#dash_left img {
display: block;
border-radius: 48px;
height: 72px;
width: 72px;
margin-left: auto;
margin-right: auto;
margin-bottom: 12px;
}
#dash_avatar_buttons {
display: flex;
}
#dash_avatar_buttons label {
margin-left: auto;
margin-right: 8px;
}
#dash_avatar_buttons button {
margin-right: auto;
}
#dash_right {
width: 100%;
margin-left: 12px;
}
#dash_right .rowitem {
border-radius: 3px;
background-color: #444444;
padding: 16px;
}
#dash_right .rowitem:not(:last-child) {
margin-bottom: 8px;
}

View File

@ -0,0 +1,34 @@
Font Awesome Free License
-------------------------
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license.
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
In the Font Awesome Free download, the CC BY 4.0 license applies to all icons
packaged as SVG and JS file types.
# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL)
In the Font Awesome Free download, the SIL OLF license applies to all icons
packaged as web and desktop font files.
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
# Attribution
Attribution is required by MIT, SIL OLF, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

View File

@ -0,0 +1,7 @@
# Font Awesome 5.0.13
Thanks for downloading Font Awesome! We're so excited you're here.
Our documentation is available online. Just head here:
https://fontawesome.com

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="rgb(187,187,187)" d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"/></svg>

After

Width:  |  Height:  |  Size: 569 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="rgb(170,170,170)" d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"/></svg>

After

Width:  |  Height:  |  Size: 569 B

View File

@ -1,5 +1,23 @@
:root { :root {
--darkest-background: #222222; --darkest-background: #222222;
--second-dark-background: #292929;
--third-dark-background: #333333;
}
@font-face {
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 400;
src: url("../fontawesome-5.0.13/webfonts/fa-regular-400.eot");
src: url("../fontawesome-5.0.13/webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../fontawesome-5.0.13/webfonts/fa-regular-400.woff2") format("woff2"), url("../fontawesome-5.0.13/webfonts/fa-regular-400.woff") format("woff"), url("../fontawesome-5.0.13/webfonts/fa-regular-400.ttf") format("truetype"), url("../fontawesome-5.0.13/webfonts/fa-regular-400.svg#fontawesome") format("svg");
}
@font-face {
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 900;
src: url("../fontawesome-5.0.13/webfonts/fa-solid-900.eot");
src: url("../fontawesome-5.0.13/webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../fontawesome-5.0.13/webfonts/fa-solid-900.woff2") format("woff2"), url("../fontawesome-5.0.13/webfonts/fa-solid-900.woff") format("woff"), url("../fontawesome-5.0.13/webfonts/fa-solid-900.ttf") format("truetype"), url("../fontawesome-5.0.13/webfonts/fa-solid-900.svg#fontawesome") format("svg");
} }
* { * {
@ -67,7 +85,8 @@ li a {
.user_box { .user_box {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
background-color: #333333; border-radius: 3px;
background-color: var(--third-dark-background);
padding-top: 11px; padding-top: 11px;
padding-bottom: 11px; padding-bottom: 11px;
padding-left: 12px; padding-left: 12px;
@ -93,7 +112,7 @@ li a {
clear: both; clear: both;
} }
#back { #back {
background: #333333; background: var(--third-dark-background);
padding: 24px; padding: 24px;
padding-top: 12px; padding-top: 12px;
clear: both; clear: both;
@ -107,11 +126,31 @@ li a {
width: 320px; width: 320px;
} }
.rowblock:not(.topic_list):not(.rowhead):not(.opthead) .rowitem { .rowblock:not(.topic_list):not(.rowhead):not(.opthead) .rowitem {
border-radius: 3px;
background-color: #444444; background-color: #444444;
display: flex; display: flex;
padding: 12px; padding: 12px;
margin-left: 12px; margin-left: 12px;
} }
.colstack_right .colstack_item.the_form {
border-radius: 3px;
background-color: #444444;
padding: 16px;
}
.colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem {
border-radius: 3px;
background-color: #444444;
padding: 16px;
}
.colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem:not(:last-child) {
margin-bottom: 8px;
}
.colstack_right .colstack_head:not(:first-child) {
margin-top: 16px;
}
.rowmsg {
margin-bottom: 8px;
}
h1, h3 { h1, h3 {
-webkit-margin-before: 0; -webkit-margin-before: 0;
@ -146,6 +185,7 @@ h1, h3 {
margin-bottom: 8px; margin-bottom: 8px;
} }
.topic_row { .topic_row {
border-radius: 3px;
background-color: #444444; background-color: #444444;
display: flex; display: flex;
} }
@ -212,6 +252,35 @@ h1, h3 {
display: none; display: none;
} }
input, select, button, .formbutton, textarea {
border-radius: 3px;
background: rgb(90,90,90);
color: rgb(200,200,200);
border: none;
padding: 4px;
}
input:focus, select:focus, textarea:focus {
outline: 1px solid rgb(120,120,120);
}
input {
background-image: url(./fa-svg/pencil-alt.svg);
background-size: 12px;
background-repeat: no-repeat;
background-position: right 10px bottom 9px;
background-position-x: right 10px;
padding: 5px;
padding-bottom: 3px;
font-size: 16px;
}
button, .formbutton {
background: rgb(110,110,210);
color: rgb(250,250,250);
font-family: "Segoe UI";
font-size: 15px;
text-align: center;
padding: 6px;
}
.pageset { .pageset {
display: flex; display: flex;
margin-top: 8px; margin-top: 8px;
@ -219,9 +288,10 @@ h1, h3 {
.pageitem { .pageitem {
font-size: 17px; font-size: 17px;
border-radius: 3px;
background-color: #444444; background-color: #444444;
padding: 7px; padding: 7px;
margin-right: 6px; margin-right: 6px;
} }
#prevFloat, #nextFloat { #prevFloat, #nextFloat {
@ -280,7 +350,7 @@ h1, h3 {
@media(min-width: 1010px) { @media(min-width: 1010px) {
.container { .container {
background-color: #292929; background-color: var(--second-dark-background);
} }
#back { #back {
width: 1000px; width: 1000px;

View File

@ -25,10 +25,16 @@
.colstack_left .colstack_head:not(:first-child) { .colstack_left .colstack_head:not(:first-child) {
margin-top: 8px; margin-top: 8px;
} }
.colstack_left .colstack_head a {
color: rgb(231, 231, 231);
}
.rowmenu { .rowmenu {
margin-bottom: 2px; margin-bottom: 2px;
font-size: 17px; font-size: 17px;
} }
.rowmenu a {
color: rgb(170, 170, 170);
}
.colstack_right { .colstack_right {
background-color: #444444; background-color: #444444;
@ -37,4 +43,58 @@
padding-right: 24px; padding-right: 24px;
padding-bottom: 24px; padding-bottom: 24px;
padding-left: 24px; padding-left: 24px;
}
.colstack_right .colstack_item.the_form {
background-color: #555555;
}
.colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem {
background-color: #555555;
}
.colstack_grid {
display: grid;
grid-gap: 8px;
grid-template-columns: repeat(3, 1fr);
}
.grid_item {
border-radius: 3px;
color: rgb(190,190,190);
background-color: #555555;
padding: 12px;
}
.rowlist.bgavatars .rowitem {
background-image: none !important;
}
.rowlist.bgavatars .bgsub {
width: 48px;
height: 48px;
}
input, select, button, .formbutton, textarea {
background: rgb(107,107,107);
color: rgb(217,217,217);
}
input:focus, select:focus, textarea:focus {
outline: 1px solid rgb(137,137,137);
}
/* ? - The background properties need to be redeclared for the new image or it won't work properly */
input {
background-image: url(./fa-svg/pencil-alt-light.svg);
background-size: 12px;
background-repeat: no-repeat;
background-position: right 10px bottom 9px;
background-position-x: right 10px;
}
input::placeholder, textarea::placeholder {
color: rgb(167,167,167);
opacity: 1; /* Firefox fix */
}
button, .formbutton {
/*background: rgb(110,110,210);
color: rgb(250,250,250);*/
}
#themeSelector select {
background: rgb(90,90,90);
color: rgb(200,200,200);
} }

View File

@ -14,10 +14,6 @@
} }
], ],
"Resources": [ "Resources": [
{
"Name":"EQCSS.js",
"Location":"global"
},
{ {
"Name":"trumbowyg/trumbowyg.min.js", "Name":"trumbowyg/trumbowyg.min.js",
"Location":"global", "Location":"global",

View File

@ -0,0 +1,51 @@
#account_dashboard .colstack_right .coldyn_block {
display: flex;
}
#dash_saved {
display: none;
}
#dash_left {
padding: 18px;
padding-right: 0px;
padding-top: 11px;
padding-left: 0px;
width: 260px;
position: relative;
}
#dash_left .rowitem {
margin-top: 0px;
}
#dash_username {
display: flex;
}
#dash_username button {
margin-left: 6px;
}
#dash_left .rowitem img {
width: 100%;
margin-top: 8px;
margin-bottom: 4px;
margin-left: 0px;
margin-right: 12px;
}
#dash_avatar_buttons {
display: flex;
}
#dash_avatar_buttons button {
margin-left: 8px;
}
#dash_right {
width: 100%;
padding: 16px;
padding-top: 3px;
padding-left: 8px;
padding-right: 0px;
}
.account_soon, .dash_security {
font-size: 13px;
color: rgba(255, 80, 80, 1);
}
.rowmenu .account_soon, .rowmenu .dash_security {
font-size: 11px;
}

View File

@ -488,7 +488,7 @@ textarea.large {
margin-top: 0px; margin-top: 0px;
} }
.formitem input { input {
background-color: var(--input-background-color); background-color: var(--input-background-color);
border: 1px solid var(--input-border-color); border: 1px solid var(--input-border-color);
color: var(--input-text-color); color: var(--input-text-color);

View File

@ -0,0 +1,30 @@
.sidebar, #dash_saved {
display: none;
}
#account_dashboard .colstack_right .coldyn_block {
display: flex;
}
#dash_left .rowitem {
border: 1px solid hsl(0,0%,85%);
}
#dash_username {
display: flex;
}
#dash_avatar_buttons {
display: flex;
}
#dash_right {
width: 100%;
margin-right: 6px;
}
#dash_right .rowitem {
margin-left: 10px;
border: 1px solid hsl(0,0%,85%);
}
#dash_right .rowitem:not(last-child) {
margin-bottom: 8px;
}
.account_soon, .dash_security {
font-size: 14px;
color: maroon;
}

View File

@ -0,0 +1,30 @@
.sidebar, #dash_saved {
display: none;
}
#account_dashboard .colstack_right .coldyn_block {
display: flex;
}
#dash_left .rowitem {
border: 1px solid hsl(0,0%,85%);
}
#dash_username {
display: flex;
}
#dash_avatar_buttons {
display: flex;
}
#dash_right {
width: 100%;
}
#dash_right .rowitem {
border: 1px solid hsl(0,0%,85%);
margin-left: 8px;
}
#dash_right .rowitem:not(:last-child) {
margin-bottom: 8px;
}
.account_soon, .dash_security {
font-size: 14px;
color: maroon;
}