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
import (
"crypto/sha256"
"crypto/subtle"
"database/sql"
"encoding/hex"
"errors"
"net/http"
"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
var ErrPasswordTooLong = errors.New("The password you selected is too long")
var ErrWrongPassword = errors.New("That's not the correct password.")
var ErrBadMFAToken = errors.New("I'm not sure where you got that from, but that's not a valid 2FA token")
var ErrWrongMFAToken = errors.New("That 2FA token isn't correct")
var ErrSecretError = errors.New("There was a glitch in the system. Please contact your local administrator.")
var ErrNoUserByName = errors.New("We couldn't find an account with that username.")
var DefaultHashAlgo = "bcrypt" // Override this in the configuration file, not here
@ -58,13 +63,16 @@ var HashPrefixes = map[string]string{
// AuthInt is the main authentication 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)
ForceLogout(uid int) error
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)
SessionCheck(w http.ResponseWriter, r *http.Request) (user *User, halt bool)
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.
@ -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.
// 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
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
err = auth.login.QueryRow(username).Scan(&uid, &realPassword, &salt)
if err == ErrNoRows {
return 0, ErrNoUserByName
return 0, ErrNoUserByName, false
} else if err != nil {
LogError(err)
return 0, ErrSecretError
return 0, ErrSecretError, false
}
err = CheckPassword(realPassword, password, salt)
if err == ErrMismatchedHashAndPassword {
return 0, ErrWrongPassword
return 0, ErrWrongPassword, false
} else if err != nil {
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
@ -141,6 +187,17 @@ func (auth *DefaultAuth) SetCookies(w http.ResponseWriter, uid int, session stri
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
func (auth *DefaultAuth) GetCookies(r *http.Request) (uid int, session string, err error) {
// Are there any session cookies..?
@ -202,6 +259,19 @@ func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) {
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) {
blasted := strings.Split(realPassword, "$")
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) {
return GenerateSafeString(24)
return GenerateStd32SafeString(14)
}
func VerifyGAuthToken(secret string, token string) (bool, error) {
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"
// 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
// 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

View File

@ -26,6 +26,8 @@ var TmplPtrMap = make(map[string]interface{})
// Anti-spam token with rotated key
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
var ErrNoRows = sql.ErrNoRows
@ -60,6 +62,8 @@ var ExecutableFileExts = StringList{
func init() {
JSTokenBox.Store("")
SessionSigningKeyBox.Store("")
OldSessionSigningKeyBox.Store("")
}
// TODO: Write a test for this

View File

@ -4,6 +4,7 @@ import (
"log"
"net/http"
"runtime/debug"
"strings"
"sync"
)
@ -114,7 +115,7 @@ func InternalErrorJSQ(err error, w http.ResponseWriter, r *http.Request, isJs bo
// ? - Add a user parameter?
func InternalErrorJS(err error, w http.ResponseWriter, r *http.Request) RouteError {
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)
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 {
w.WriteHeader(500)
_, _ = w.Write([]byte(`{"errmsg":"` + errmsg + `"}`))
writeJsonError(errmsg, w)
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 {
w.WriteHeader(500)
_, _ = w.Write([]byte(`{"errmsg": "` + errmsg + `"}`))
writeJsonError(errmsg, w)
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 {
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()
}
@ -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 {
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()
}
@ -246,7 +247,7 @@ func LoginRequired(w http.ResponseWriter, r *http.Request, user User) RouteError
// nolint
func LoginRequiredJS(w http.ResponseWriter, r *http.Request, user User) RouteError {
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()
}
@ -297,10 +298,15 @@ func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.Response
// CustomErrorJS is the pure JSON version of CustomError
func CustomErrorJS(errmsg string, errcode int, w http.ResponseWriter, r *http.Request, user User) RouteError {
w.WriteHeader(errcode)
_, _ = w.Write([]byte(`{"errmsg":"` + errmsg + `"}`))
writeJsonError(errmsg, w)
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) {
// TODO: What to do about this hook?
if RunPreRenderHook("pre_render_error", w, r, &pi.Header.CurrentUser, &pi) {

View File

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

View File

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

View File

@ -26,6 +26,8 @@ func prefix0(otp string) string {
}
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
key, err := base32.StdEncoding.DecodeString(strings.ToUpper(secret))
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
// 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 {
}
@ -8,48 +9,35 @@ func NewNullTopicCache() *NullTopicCache {
return &NullTopicCache{}
}
// nolint
func (mts *NullTopicCache) Get(id int) (*Topic, error) {
return nil, ErrNoRows
}
func (mts *NullTopicCache) GetUnsafe(id int) (*Topic, error) {
return nil, ErrNoRows
}
func (mts *NullTopicCache) Set(_ *Topic) error {
return nil
}
func (mts *NullTopicCache) Add(item *Topic) error {
_ = item
func (mts *NullTopicCache) Add(_ *Topic) error {
return nil
}
// TODO: Make these length increments thread-safe. Ditto for the other DataStores
func (mts *NullTopicCache) AddUnsafe(item *Topic) error {
_ = item
func (mts *NullTopicCache) AddUnsafe(_ *Topic) error {
return nil
}
// TODO: Make these length decrements thread-safe. Ditto for the other DataStores
func (mts *NullTopicCache) Remove(id int) error {
return nil
}
func (mts *NullTopicCache) RemoveUnsafe(id int) error {
return nil
}
func (mts *NullTopicCache) Flush() {
}
func (mts *NullTopicCache) Length() int {
return 0
}
func (mts *NullTopicCache) SetCapacity(_ int) {
}
func (mts *NullTopicCache) GetCapacity() int {
return 0
}

View File

@ -1,5 +1,6 @@
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 {
}
@ -8,50 +9,38 @@ func NewNullUserCache() *NullUserCache {
return &NullUserCache{}
}
// nolint
func (mus *NullUserCache) Get(id int) (*User, error) {
return nil, ErrNoRows
}
func (mus *NullUserCache) BulkGet(_ []int) (list []*User) {
return nil
}
func (mus *NullUserCache) GetUnsafe(id int) (*User, error) {
return nil, ErrNoRows
}
func (mus *NullUserCache) Set(_ *User) error {
return nil
}
func (mus *NullUserCache) Add(item *User) error {
_ = item
func (mus *NullUserCache) Add(_ *User) error {
return nil
}
func (mus *NullUserCache) AddUnsafe(item *User) error {
_ = item
func (mus *NullUserCache) AddUnsafe(_ *User) error {
return nil
}
func (mus *NullUserCache) Remove(id int) error {
return nil
}
func (mus *NullUserCache) RemoveUnsafe(id int) error {
return nil
}
func (mus *NullUserCache) Flush() {
}
func (mus *NullUserCache) Length() int {
return 0
}
func (mus *NullUserCache) SetCapacity(_ int) {
}
func (mus *NullUserCache) GetCapacity() int {
return 0
}

View File

@ -142,6 +142,11 @@ type EmailListPage struct {
Something interface{}
}
type AccountDashPage struct {
*Header
MFASetup bool
}
type PanelStats struct {
Users 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 InvalidForum = []byte("<span style='color: red;'>[Invalid Forum]</span>")
var unknownMedia = []byte("<span style='color: red;'>[Unknown Media]</span>")
var UrlOpen = []byte("<a href='")
var UrlOpen2 = []byte("'>")
var URLOpen = []byte("<a href='")
var URLOpen2 = []byte("'>")
var bytesSinglequote = []byte("'")
var bytesGreaterthan = []byte(">")
var urlMention = []byte(" class='mention'")
var UrlClose = []byte("</a>")
var URLClose = []byte("</a>")
var imageOpen = []byte("<a href=\"")
var imageOpen2 = []byte("\"><img src='")
var imageClose = []byte("' class='postImage' /></a>")
@ -319,13 +319,13 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
continue
}
outbytes = append(outbytes, UrlOpen...)
outbytes = append(outbytes, URLOpen...)
var urlBit = []byte(BuildTopicURL("", tid))
outbytes = append(outbytes, urlBit...)
outbytes = append(outbytes, UrlOpen2...)
outbytes = append(outbytes, URLOpen2...)
var tidBit = []byte("#tid-" + strconv.Itoa(tid))
outbytes = append(outbytes, tidBit...)
outbytes = append(outbytes, UrlClose...)
outbytes = append(outbytes, URLClose...)
lastItem = i
} else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) {
outbytes = append(outbytes, msgbytes[lastItem:i]...)
@ -341,13 +341,13 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
continue
}
outbytes = append(outbytes, UrlOpen...)
outbytes = append(outbytes, URLOpen...)
var urlBit = []byte(BuildTopicURL("", topic.ID))
outbytes = append(outbytes, urlBit...)
outbytes = append(outbytes, UrlOpen2...)
outbytes = append(outbytes, URLOpen2...)
var ridBit = []byte("#rid-" + strconv.Itoa(rid))
outbytes = append(outbytes, ridBit...)
outbytes = append(outbytes, UrlClose...)
outbytes = append(outbytes, URLClose...)
lastItem = i
} else if bytes.Equal(msgbytes[i+1:i+5], []byte("fid-")) {
outbytes = append(outbytes, msgbytes[lastItem:i]...)
@ -362,13 +362,13 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
continue
}
outbytes = append(outbytes, UrlOpen...)
outbytes = append(outbytes, URLOpen...)
var urlBit = []byte(BuildForumURL("", fid))
outbytes = append(outbytes, urlBit...)
outbytes = append(outbytes, UrlOpen2...)
outbytes = append(outbytes, URLOpen2...)
var fidBit = []byte("#fid-" + strconv.Itoa(fid))
outbytes = append(outbytes, fidBit...)
outbytes = append(outbytes, UrlClose...)
outbytes = append(outbytes, URLClose...)
lastItem = i
} else {
// TODO: Forum Shortcode Link
@ -387,7 +387,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
continue
}
outbytes = append(outbytes, UrlOpen...)
outbytes = append(outbytes, URLOpen...)
var urlBit = []byte(menUser.Link)
outbytes = append(outbytes, urlBit...)
outbytes = append(outbytes, bytesSinglequote...)
@ -395,7 +395,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
outbytes = append(outbytes, bytesGreaterthan...)
var uidBit = []byte("@" + menUser.Name)
outbytes = append(outbytes, uidBit...)
outbytes = append(outbytes, UrlClose...)
outbytes = append(outbytes, URLClose...)
lastItem = 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' {
@ -463,11 +463,11 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
continue
}
outbytes = append(outbytes, UrlOpen...)
outbytes = append(outbytes, URLOpen...)
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, UrlClose...)
outbytes = append(outbytes, URLClose...)
i += urlLen
lastItem = i
}

View File

@ -1,7 +1,11 @@
package common
import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"strconv"
"strings"
)
@ -9,13 +13,13 @@ import (
var Site = &site{Name: "Magical Fairy Land", Language: "english"}
// DbConfig holds the database configuration
var DbConfig = dbConfig{Host: "localhost"}
var DbConfig = &dbConfig{Host: "localhost"}
// 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
var Dev devConfig
var Dev = new(devConfig)
type site struct {
ShortName string
@ -28,6 +32,8 @@ type site struct {
EnableEmails bool
HasProxy bool
Language string
MaxRequestSize int // Alias, do not modify, will be overwritten
}
type dbConfig struct {
@ -53,9 +59,11 @@ type config struct {
SslFullchain string
HashAlgo string // Defaults to bcrypt, and in the future, possibly something stronger
MaxRequestSizeStr string
MaxRequestSize int
CacheTopicUser int
UserCache string
UserCacheCapacity int
TopicCache string
TopicCacheCapacity int
SMTPServer string
@ -64,9 +72,9 @@ type config struct {
SMTPPort string
//SMTPEnableTLS bool
DefaultRoute string
DefaultGroup int
ActivationGroup int
DefaultPath string
DefaultGroup int // Should be a setting in the database
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
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
@ -87,7 +95,35 @@ type devConfig struct {
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)
Site.Host = Site.URL
if Site.Port != "80" && Site.Port != "443" {
@ -96,6 +132,37 @@ func ProcessConfig() error {
Site.URL = strings.TrimSuffix(Site.URL, ":")
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
if Config.MaxTopicTitleLength == 0 {

View File

@ -434,7 +434,8 @@ func InitTemplates() error {
if !ok {
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{} {

View File

@ -212,6 +212,15 @@ func (theme *Theme) MapTemplates() {
default:
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:
switch sTmplPtr := sourceTmplPtr.(type) {
case *func(ErrorPage, io.Writer) error:

View File

@ -250,6 +250,13 @@ func ResetTemplateOverrides() {
default:
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:
switch dPtr := destTmplPtr.(type) {
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:
var tmpl = *tmplO
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:
var tmpl = *tmplO
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)
case func(IPSearchPage, io.Writer) error:
return tmplO(pi.(IPSearchPage), w)
case func(AccountDashPage, io.Writer) error:
return tmplO(pi.(AccountDashPage), w)
case func(ErrorPage, io.Writer) error:
return tmplO(pi.(ErrorPage), w)
case func(Page, io.Writer) error:

View File

@ -5,6 +5,7 @@ import (
"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 {
Get(id int) (*Topic, error)
GetUnsafe(id int) (*Topic, error)
@ -19,6 +20,7 @@ type TopicCache interface {
GetCapacity() int
}
// MemoryTopicCache stores and pulls topics out of the current process' memory
type MemoryTopicCache struct {
items map[int]*Topic
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) {
mts.RLock()
item, ok := mts.items[id]
@ -45,6 +48,7 @@ func (mts *MemoryTopicCache) Get(id int) (*Topic, error) {
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) {
item, ok := mts.items[id]
if ok {
@ -53,6 +57,7 @@ func (mts *MemoryTopicCache) GetUnsafe(id int) (*Topic, error) {
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 {
mts.Lock()
_, ok := mts.items[item.ID]
@ -69,42 +74,56 @@ func (mts *MemoryTopicCache) Set(item *Topic) error {
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 {
mts.Lock()
if int(mts.length) >= mts.capacity {
mts.Unlock()
return ErrStoreCapacityOverflow
}
mts.Lock()
mts.items[item.ID] = item
mts.Unlock()
atomic.AddInt64(&mts.length, 1)
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 {
if int(mts.length) >= mts.capacity {
return ErrStoreCapacityOverflow
}
mts.items[item.ID] = item
atomic.AddInt64(&mts.length, 1)
mts.length = int64(len(mts.items))
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 {
mts.Lock()
_, ok := mts.items[id]
if !ok {
mts.Unlock()
return ErrNoRows
}
delete(mts.items, id)
mts.Unlock()
atomic.AddInt64(&mts.length, -1)
return nil
}
// RemoveUnsafe is the unsafe version of Remove. THIS METHOD IS NOT THREAD-SAFE.
func (mts *MemoryTopicCache) RemoveUnsafe(id int) error {
_, ok := mts.items[id]
if !ok {
return ErrNoRows
}
delete(mts.items, id)
atomic.AddInt64(&mts.length, -1)
return nil
}
// Flush removes all the topics from the cache, useful for tests.
func (mts *MemoryTopicCache) Flush() {
mts.Lock()
mts.items = make(map[int]*Topic)
@ -118,10 +137,13 @@ func (mts *MemoryTopicCache) Length() int {
return int(mts.length)
}
// SetCapacity sets the maximum number of topics which this cache can hold
func (mts *MemoryTopicCache) SetCapacity(capacity int) {
// Ints are moved in a single instruction, so this should be thread-safe
mts.capacity = capacity
}
// GetCapacity returns the maximum number of topics this cache can hold
func (mts *MemoryTopicCache) GetCapacity() int {
return mts.capacity
}

View File

@ -61,6 +61,7 @@ type UserStmts struct {
setUsername *sql.Stmt
incrementTopics *sql.Stmt
updateLevel *sql.Stmt
update *sql.Stmt
// TODO: Split these into a sub-struct
incrementScore *sql.Stmt
@ -88,6 +89,8 @@ func init() {
setUsername: acc.Update("users").Set("name = ?").Where(where).Prepare(),
incrementTopics: acc.SimpleUpdate("users", "topics = topics + ?", 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),
incrementPosts: acc.SimpleUpdate("users", "posts = posts + ?", where),
incrementBigposts: acc.SimpleUpdate("users", "posts = posts + ?, bigposts = bigposts + ?", where),
@ -253,6 +256,10 @@ func (user *User) UpdateIP(host string) error {
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) {
var mod int
baseScore := 1

View File

@ -5,6 +5,7 @@ import (
"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 {
Get(id int) (*User, error)
GetUnsafe(id int) (*User, error)
@ -20,6 +21,7 @@ type UserCache interface {
GetCapacity() int
}
// MemoryUserCache stores and pulls users out of the current process' memory
type MemoryUserCache struct {
items map[int]*User
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) {
mus.RLock()
item, ok := mus.items[id]
@ -46,6 +49,7 @@ func (mus *MemoryUserCache) Get(id int) (*User, error) {
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) {
list = make([]*User, len(ids))
mus.RLock()
@ -56,6 +60,7 @@ func (mus *MemoryUserCache) BulkGet(ids []int) (list []*User) {
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) {
item, ok := mus.items[id]
if ok {
@ -64,6 +69,7 @@ func (mus *MemoryUserCache) GetUnsafe(id int) (*User, error) {
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 {
mus.Lock()
user, ok := mus.items[item.ID]
@ -81,17 +87,21 @@ func (mus *MemoryUserCache) Set(item *User) error {
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 {
mus.Lock()
if int(mus.length) >= mus.capacity {
mus.Unlock()
return ErrStoreCapacityOverflow
}
mus.Lock()
mus.items[item.ID] = item
mus.length = int64(len(mus.items))
mus.Unlock()
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 {
if int(mus.length) >= mus.capacity {
return ErrStoreCapacityOverflow
@ -101,6 +111,7 @@ func (mus *MemoryUserCache) AddUnsafe(item *User) error {
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 {
mus.Lock()
_, ok := mus.items[id]
@ -114,6 +125,7 @@ func (mus *MemoryUserCache) Remove(id int) error {
return nil
}
// RemoveUnsafe is the unsafe version of Remove. THIS METHOD IS NOT THREAD-SAFE.
func (mus *MemoryUserCache) RemoveUnsafe(id int) error {
_, ok := mus.items[id]
if !ok {
@ -124,6 +136,7 @@ func (mus *MemoryUserCache) RemoveUnsafe(id int) error {
return nil
}
// Flush removes all the users from the cache, useful for tests.
func (mus *MemoryUserCache) Flush() {
mus.Lock()
mus.items = make(map[int]*User)
@ -137,10 +150,13 @@ func (mus *MemoryUserCache) Length() int {
return int(mus.length)
}
// SetCapacity sets the maximum number of users which this cache can hold
func (mus *MemoryUserCache) SetCapacity(capacity int) {
// Ints are moved in a single instruction, so this should be thread-safe
mus.capacity = capacity
}
// GetCapacity returns the maximum number of users this cache can hold
func (mus *MemoryUserCache) GetCapacity() int {
return mus.capacity
}

View File

@ -8,6 +8,7 @@ package common
import (
"crypto/rand"
"encoding/base32"
"encoding/base64"
"errors"
"fmt"
@ -41,7 +42,7 @@ func (version *Version) String() (out string) {
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
func GenerateSafeString(length int) (string, error) {
rb := make([]byte, length)
@ -52,6 +53,17 @@ func GenerateSafeString(length int) (string, error) {
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
func RelativeTimeFromString(in string) (string, error) {
if in == "" {
@ -147,6 +159,27 @@ func ConvertByteInUnit(bytes float64, unit string) (count float64) {
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: Re-add T as int64
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")
var ucache common.UserCache
var tcache common.TopicCache
if common.Config.CacheTopicUser == common.CACHE_STATIC {
if common.Config.UserCache == "static" {
ucache = common.NewMemoryUserCache(common.Config.UserCacheCapacity)
}
var tcache common.TopicCache
if common.Config.TopicCache == "static" {
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
updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt
updateUser *sql.Stmt
updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt
updateEmail *sql.Stmt
@ -141,14 +140,6 @@ func _gen_mssql() (err error) {
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.")
stmts.updateGroupPerms, err = db.Prepare("UPDATE [users_groups] SET [permissions] = ? WHERE [gid] = ?")
if err != nil {

View File

@ -23,7 +23,6 @@ type Stmts struct {
updatePlugin *sql.Stmt
updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt
updateUser *sql.Stmt
updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt
updateEmail *sql.Stmt
@ -131,13 +130,6 @@ func _gen_mysql() (err error) {
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.")
stmts.updateGroupPerms, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?")
if err != nil {

View File

@ -16,7 +16,6 @@ type Stmts struct {
updatePlugin *sql.Stmt
updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt
updateUser *sql.Stmt
updateGroupPerms *sql.Stmt
updateGroup *sql.Stmt
updateEmail *sql.Stmt
@ -87,13 +86,6 @@ func _gen_pgsql() (err error) {
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.")
stmts.updateGroupPerms, err = db.Prepare("UPDATE `users_groups` SET `permissions` = ? WHERE `gid` = ?")
if err != nil {

View File

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

View File

@ -2,20 +2,21 @@
package main
var dbTablePrimaryKeys = map[string]string{
"topics": "tid",
"attachments": "attachID",
"menus": "mid",
"users_groups":"gid",
"users_groups_scheduler": "uid",
"registration_logs": "rlid",
"word_filters": "wfid",
"menu_items": "miid",
"polls": "pollID",
"attachments":"attachID",
"users_replies":"rid",
"activity_stream": "asid",
"menu_items":"miid",
"pages":"pid",
"polls":"pollID",
"activity_stream":"asid",
"users_groups_scheduler":"uid",
"replies":"rid",
"revisions": "reviseID",
"word_filters":"wfid",
"menus":"mid",
"registration_logs":"rlid",
"users":"uid",
"users_2fa_keys":"uid",
"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
func BenchmarkParserSerial(b *testing.B) {
b.ReportAllocs()

View File

@ -96,79 +96,65 @@ func main() {
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() {
// Site Info
common.Site.ShortName = "` + siteShortName + `" // This should be less than three letters to fit in the navbar
common.Site.Name = "` + siteName + `"
common.Site.Email = ""
common.Site.URL = "` + siteURL + `"
common.Site.Port = "` + serverPort + `"
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.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
"TestAdapter": "` + adap.Name() + `",
"TestHost": "",
"TestUsername": "",
"TestPassword": "",
"TestDbname": "",
"TestPort": ""
},
"Dev": {
"DebugMode":true,
"SuperDebug":false
}
`)
}`)
//"Noavatar": "https://api.adorable.io/avatars/{width}/{id}@{site_url}.png" Maybe allow this sort of syntax?
fmt.Println("Opening the configuration file")
configFile, err := os.Create("./config/config.go")
configFile, err := os.Create("./config/config.json")
if err != nil {
abortError(err)
return

View File

@ -81,8 +81,10 @@
"login":"Login",
"register":"Registration",
"ip_search":"IP Search",
"account_username":"Edit Username",
"account_avatar":"Edit Avatar",
"account":"My Account",
"account_password":"Edit Password",
"account_mfa":"Manage 2FA",
"account_mfa_setup":"Setup 2FA",
"account_email":"Email Manager",
"panel_dashboard":"Control Panel Dashboard",
@ -244,19 +246,20 @@
"NoticePhrases": {
"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_avatar_updated":"Your avatar was successfully updated",
"account_username_updated":"Your username was successfully updated",
"account_avatar_updated":"Your avatar was successfully updated.",
"account_username_updated":"Your username was successfully updated.",
"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_deleted":"The forum was successfully deleted",
"panel_forum_updated":"The forum was successfully updated",
"panel_forum_perms_updated":"The forum permissions were successfully updated",
"panel_user_updated":"The user was successfully updated",
"panel_page_created":"The page was successfully created",
"panel_page_updated":"The page was successfully updated",
"panel_page_deleted":"The page was successfully deleted"
"panel_forum_created":"The forum was successfully created.",
"panel_forum_deleted":"The forum was successfully deleted.",
"panel_forum_updated":"The forum was successfully updated.",
"panel_forum_perms_updated":"The forum permissions were successfully updated.",
"panel_user_updated":"The user was successfully updated.",
"panel_page_created":"The page was successfully created.",
"panel_page_updated":"The page was successfully updated.",
"panel_page_deleted":"The page was successfully deleted."
},
"TmplPhrases": {
@ -303,11 +306,13 @@
"panel_rank_guests":"Guests",
"panel_rank_members":"Members",
"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_preset_public":"Public",
"panel_active_hidden":"Hidden",
@ -347,6 +352,10 @@
"login_submit_button":"Login",
"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_account_name":"Account Name",
"register_account_email":"Email",
@ -356,16 +365,19 @@
"register_submit_button":"Create Account",
"account_menu_head":"My Account",
"account_menu_avatar":"Avatar",
"account_menu_username":"Username",
"account_menu_password":"Password",
"account_menu_email":"Email",
"account_menu_security":"Security",
"account_menu_notifications":"Notifications",
"account_avatar_head":"Edit Avatar",
"account_avatar_upload_label":"Upload Avatar",
"account_avatar_update_button":"Update",
"account_coming_soon":"Coming Soon",
"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_primary":"Primary",
@ -373,17 +385,23 @@
"account_email_verified":"Verified",
"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_current_password":"Current Password",
"account_password_new_password":"New Password",
"account_password_confirm_password":"Confirm Password",
"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_continue":"Continue",
@ -603,13 +621,6 @@
"panel_forums_create_description":"Where all the super secret stuff happens",
"panel_forums_active_label":"Active",
"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_forum_head_suffix":" Forum",

21
main.go
View File

@ -21,7 +21,6 @@ import (
"./common"
"./common/counters"
"./config"
"./query_gen/lib"
"./routes"
"github.com/fsnotify/fsnotify"
@ -106,6 +105,10 @@ func afterDBInit() (err error) {
}
log.Print("Initialising the stores")
common.MFAstore, err = common.NewSQLMFAStore(acc)
if err != nil {
return err
}
common.Pages, err = common.NewDefaultPageStore(acc)
if err != nil {
return err
@ -207,7 +210,6 @@ func main() {
return
}
}()*/
config.Config()
// TODO: Have a file for each run with the time/date the server started as the file name?
// TODO: Log panics with recover()
@ -227,6 +229,11 @@ func main() {
}
common.JSTokenBox.Store(jsToken)
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 {
@ -344,6 +351,7 @@ func main() {
}
}
// TODO: Write tests for these
// Run this goroutine once every half second
halfSecondTicker := time.NewTicker(time.Second / 2)
secondTicker := time.NewTicker(time.Second)
@ -394,11 +402,20 @@ func main() {
runHook("after_fifteen_minute_tick")
case <-hourTicker.C:
runHook("before_hour_tick")
jsToken, err := common.GenerateSafeString(80)
if err != nil {
common.LogError(err)
}
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)
runHook("after_hour_tick")
}

View File

@ -476,171 +476,6 @@ func routePanelPluginsInstall(w http.ResponseWriter, r *http.Request, user commo
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 {
header, stats, ferr := common.PanelUserCheck(w, r, &user)
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) {
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 {
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) {
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 {
return common.InternalError(err, w, r)
}

View File

@ -11,7 +11,6 @@ import (
"strconv"
"../common"
"../config"
"../query_gen/lib"
_ "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 != "" {
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 {
log.Fatal(err)
}

View File

@ -13,6 +13,7 @@ func init() {
addPatch(2, patch2)
addPatch(3, patch3)
addPatch(4, patch4)
addPatch(5, patch5)
}
func patch0(scanner *bufio.Scanner) (err error) {
@ -447,3 +448,64 @@ func patch4(scanner *bufio.Scanner) error {
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
}
outbytes = append(outbytes, common.UrlOpen...)
outbytes = append(outbytes, common.URLOpen...)
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, common.UrlClose...)
outbytes = append(outbytes, common.URLClose...)
i += 6
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];
// Iterate over the files
let totalSize = 0;
for(let i = 0; i < files.length; i++) {
console.log("files[" + i + "]",files[i]);
totalSize += files[i]["size"];
let reader = new FileReader();
reader.onload = function(e) {
var fileDock = document.getElementById("upload_file_dock");
@ -540,6 +543,9 @@ $(document).ready(function(){
}
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");

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, "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})
@ -301,8 +301,6 @@ func writeUpdates(adapter qgen.Adapter) error {
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("updateGroup").Table("users_groups").Set("name = ?, tag = ?").Where("gid = ?").Parse()

View File

@ -62,7 +62,6 @@ func createTables(adapter qgen.Adapter) error {
},
)
/*
qgen.Install.CreateTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci",
[]qgen.DBTableColumn{
qgen.DBTableColumn{"uid", "int", 0, false, false, ""},
@ -75,12 +74,12 @@ func createTables(adapter qgen.Adapter) error {
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"},
},
)
*/
// 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?

View File

@ -416,9 +416,9 @@ func (router *GenRouter) SuspiciousRequest(req *http.Request, prepend string) {
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: SetDefaultRoute
// TODO: GetDefaultRoute
// TODO: Pass the default path or config struct to the router rather than accessing it via a package global
// TODO: SetDefaultPath
// TODO: GetDefaultPath
func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Redirect www. requests to the right place
if req.Host == "www." + common.Site.Host {
@ -457,6 +457,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
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
prefix = req.URL.Path[0:strings.IndexByte(req.URL.Path[1:],'/') + 1]
if req.URL.Path[len(req.URL.Path) - 1] != '/' {
@ -671,21 +676,8 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
return*/
}
if extraData != "" {
common.NotFound(w,req,nil)
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:
// A fallback for the routes which haven't been converted to the new router yet or plugins
router.RLock()

View File

@ -40,12 +40,15 @@ func buildUserRoutes() {
userGroup := newRouteGroup("/user/")
userGroup.Routes(
View("routes.ViewProfile", "/user/").LitBefore("req.URL.Path += extraData"),
MemberView("routes.AccountEditCritical", "/user/edit/critical/"),
Action("routes.AccountEditCriticalSubmit", "/user/edit/critical/submit/"), // TODO: Full test this
MemberView("routes.AccountEditAvatar", "/user/edit/avatar/"),
MemberView("routes.AccountEdit", "/user/edit/"),
MemberView("routes.AccountEditPassword", "/user/edit/password/"),
Action("routes.AccountEditPasswordSubmit", "/user/edit/password/submit/"), // TODO: Full test this
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
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/"),
Action("routes.AccountEditEmailTokenSubmit", "/user/edit/token/", "extraData"),
)
@ -95,7 +98,6 @@ func buildReplyRoutes() {
// TODO: Move these into /user/?
func buildProfileReplyRoutes() {
//router.HandleFunc("/user/edit/submit/", routeLogout) // routeLogout? what on earth? o.o
pReplyGroup := newRouteGroup("/profile/")
pReplyGroup.Routes(
Action("routes.ProfileReplyCreateSubmit", "/profile/reply/create/"), // TODO: Add /submit/ to the end
@ -122,6 +124,8 @@ func buildAccountRoutes() {
View("routes.AccountRegister", "/accounts/create/"),
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?
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/"),
)
addRouteGroup(accReplyGroup)
@ -172,9 +176,9 @@ func buildPanelRoutes() {
Action("routePanelPluginsDeactivate", "/panel/plugins/deactivate/", "extraData"),
Action("routePanelPluginsInstall", "/panel/plugins/install/", "extraData"),
View("routePanelUsers", "/panel/users/"),
View("routePanelUsersEdit", "/panel/users/edit/", "extraData"),
Action("routePanelUsersEditSubmit", "/panel/users/edit/submit/", "extraData"),
View("panel.Users", "/panel/users/"),
View("panel.UsersEdit", "/panel/users/edit/", "extraData"),
Action("panel.UsersEditSubmit", "/panel/users/edit/submit/", "extraData"),
View("panel.AnalyticsViews", "/panel/analytics/views/").Before("ParseForm"),
View("panel.AnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"),

View File

@ -2,6 +2,7 @@ package routes
import (
"crypto/sha256"
"crypto/subtle"
"database/sql"
"encoding/hex"
"io"
@ -48,16 +49,30 @@ func AccountLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User
}
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 {
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)
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
if user.Session == "" {
@ -79,6 +94,97 @@ func AccountLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User
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 {
common.Auth.Logout(w, user.ID)
http.Redirect(w, r, "/", http.StatusSeeOther)
@ -233,14 +339,57 @@ func AccountRegisterSubmit(w http.ResponseWriter, r *http.Request, user common.U
return nil
}
// TODO: Rename this
func AccountEditCritical(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
header, ferr := common.UserCheck(w, r, &user)
// TODO: Figure a way of making this into middleware?
func accountEditHead(titlePhrase string, w http.ResponseWriter, r *http.Request, user *common.User) (*common.Header, common.RouteError) {
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 {
return ferr
}
// TODO: Add a phrase for this
header.Title = "Edit Password"
pi := common.Page{header, tList, nil}
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
}
// TODO: Rename this
func AccountEditCriticalSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
// TODO: Require re-authentication if the user hasn't logged in in a while
func AccountEditPasswordSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
_, ferr := common.SimpleUserCheck(w, r, &user)
if ferr != nil {
return ferr
@ -291,27 +440,6 @@ func AccountEditCriticalSubmit(w http.ResponseWriter, r *http.Request, user comm
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 {
_, ferr := common.SimpleUserCheck(w, r, &user)
if ferr != nil {
@ -377,28 +505,7 @@ func AccountEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user common
if err != nil {
return common.InternalError(err, w, r)
}
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)
}
http.Redirect(w, r, "/user/edit/?avatar_updated=1", http.StatusSeeOther)
return nil
}
@ -409,22 +516,140 @@ func AccountEditUsernameSubmit(w http.ResponseWriter, r *http.Request, user comm
}
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)
if err != nil {
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
}
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 {
return ferr
}
header.Title = common.GetTitlePhrase("account_email")
emails, err := common.Emails.GetEmailsByUser(&user)
if err != nil {
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) {
header, stats, ferr := common.PanelUserCheck(w, r, user)
basePage, ferr := buildBasePage(w, r, user, "analytics", "analytics")
if ferr != nil {
return nil, ferr
}
header.Title = common.GetTitlePhrase("panel_analytics")
header.AddSheet("chartist/chartist.min.css")
header.AddScript("chartist/chartist.min.js")
header.AddScript("analytics.js")
return &common.BasePanelPage{header, stats, "analytics", common.ReportForumID}, nil
basePage.AddSheet("chartist/chartist.min.css")
basePage.AddScript("chartist/chartist.min.js")
basePage.AddScript("analytics.js")
return basePage, nil
}
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 {
header, stats, ferr := common.PanelUserCheck(w, r, &user)
basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics")
if ferr != nil {
return ferr
}
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
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)
}
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 {
return ferr
}
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
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)
}
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 {
return ferr
}
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
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)
}
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 {
return ferr
}
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
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)
}
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 {
return ferr
}
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
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)
}
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 {
return ferr
}
header.Title = common.GetTitlePhrase("panel_analytics")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
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)
}

View File

@ -11,11 +11,10 @@ import (
)
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 {
return ferr
}
header.Title = common.GetTitlePhrase("panel_backups")
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
@ -25,7 +24,7 @@ func Backups(w http.ResponseWriter, r *http.Request, user common.User, backupURL
if ext == ".sql" {
info, err := os.Stat("./backups/" + backupURL)
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
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)
return nil
}
return common.NotFound(w, r, header)
return common.NotFound(w, r, basePage.Header)
}
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()})
}
pi := common.PanelBackupPage{&common.BasePanelPage{header, stats, "backups", common.ReportForumID}, backupList}
pi := common.PanelBackupPage{basePage, backupList}
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
}
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 {
header, stats, ferr := common.PanelUserCheck(w, r, &user)
basePage, ferr := buildBasePage(w, r, &user, "debug", "debug")
if ferr != nil {
return ferr
}
header.Title = common.GetTitlePhrase("panel_debug")
goVersion := runtime.Version()
dbVersion := qgen.Builder.DbVersion()
@ -38,6 +37,6 @@ func Debug(w http.ResponseWriter, r *http.Request, user common.User) common.Rout
// Disk I/O?
// 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)
}

View File

@ -11,14 +11,13 @@ import (
)
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 {
return ferr
}
if !user.Perms.ManageForums {
return common.NoPermissions(w, r, user)
}
header.Title = common.GetTitlePhrase("panel_forums")
// TODO: Paginate this?
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" {
header.AddNotice("panel_forum_created")
basePage.AddNotice("panel_forum_created")
} else if r.FormValue("deleted") == "1" {
header.AddNotice("panel_forum_deleted")
basePage.AddNotice("panel_forum_deleted")
} 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)
}
@ -76,14 +75,13 @@ func ForumsCreateSubmit(w http.ResponseWriter, r *http.Request, user common.User
// TODO: Revamp this
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 {
return ferr
}
if !user.Perms.ManageForums {
return common.NoPermissions(w, r, user)
}
header.Title = common.GetTitlePhrase("panel_delete_forum")
fid, err := strconv.Atoi(sfid)
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?"
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) {
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 {
header, stats, ferr := common.PanelUserCheck(w, r, &user)
basePage, ferr := buildBasePage(w, r, &user, "edit_forum", "forums")
if ferr != nil {
return ferr
}
if !user.Perms.ManageForums {
return common.NoPermissions(w, r, user)
}
header.Title = common.GetTitlePhrase("panel_edit_forum")
fid, err := strconv.Atoi(sfid)
if err != nil {
@ -158,7 +155,6 @@ func ForumsEdit(w http.ResponseWriter, r *http.Request, user common.User, sfid s
} else if err != nil {
return common.InternalError(err, w, r)
}
if forum.Preset == "" {
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" {
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) {
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 {
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 {
header, stats, ferr := common.PanelUserCheck(w, r, &user)
basePage, ferr := buildBasePage(w, r, &user, "edit_forum", "forums")
if ferr != nil {
return ferr
}
if !user.Perms.ManageForums {
return common.NoPermissions(w, r, user)
}
header.Title = common.GetTitlePhrase("panel_edit_forum")
fid, gid, err := forumPermsExtractDash(paramList)
if err != nil {
@ -350,14 +345,14 @@ func ForumsEditPermsAdvance(w http.ResponseWriter, r *http.Request, user common.
addNameLangToggle("MoveTopic", forumPerms.MoveTopic)
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) {
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 {
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 {
header, stats, ferr := common.PanelUserCheck(w, r, &user)
basePage, ferr := buildBasePage(w, r, &user, "registration_logs", "logs")
if ferr != nil {
return ferr
}
header.Title = common.GetTitlePhrase("panel_registration_logs")
logCount := common.RegLogs.GlobalCount()
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)
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)
}
@ -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 {
header, stats, ferr := common.PanelUserCheck(w, r, &user)
basePage, ferr := buildBasePage(w, r, &user, "mod_logs", "logs")
if ferr != nil {
return ferr
}
header.Title = common.GetTitlePhrase("panel_mod_logs")
logCount := common.ModLogs.GlobalCount()
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)
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)
}
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 {
return ferr
}
header.Title = common.GetTitlePhrase("panel_admin_logs")
logCount := common.ModLogs.GlobalCount()
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)
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)
}

View File

@ -9,16 +9,15 @@ import (
)
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 {
return ferr
}
header.Title = common.GetTitlePhrase("panel_pages")
if r.FormValue("created") == "1" {
header.AddNotice("panel_page_created")
basePage.AddNotice("panel_page_created")
} else if r.FormValue("deleted") == "1" {
header.AddNotice("panel_page_deleted")
basePage.AddNotice("panel_page_deleted")
}
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)
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)
}
@ -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 {
header, stats, ferr := common.PanelUserCheck(w, r, &user)
basePage, ferr := buildBasePage(w, r, &user, "pages_edit", "pages")
if ferr != nil {
return ferr
}
header.Title = common.GetTitlePhrase("panel_pages_edit")
if r.FormValue("updated") == "1" {
header.AddNotice("panel_page_updated")
basePage.AddNotice("panel_page_updated")
}
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)
if err == sql.ErrNoRows {
return common.NotFound(w, r, header)
return common.NotFound(w, r, basePage.Header)
} else if err != nil {
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)
}

View File

@ -11,16 +11,15 @@ import (
)
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 {
return ferr
}
if !user.Perms.EditSettings {
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 {
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)})
}
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)
}
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 {
return ferr
}
if !user.Perms.EditSettings {
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 {
return common.LocalError("The setting you want to edit doesn't exist.", w, r, user)
} 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)}
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)
}

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],[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],[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],[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);

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`,`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`,`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`,`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);

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","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","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","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);

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",
"MinGoVersion":"1.10",
"MinVersion":""

View File

@ -1,15 +1,13 @@
<nav class="colstack_left">
<div class="colstack_item colstack_head rowhead menuhead">
<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 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/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/password/">{{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/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 **/}}
</div>
</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>
<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="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>

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">
var session = "{{.CurrentUser.Session}}";
var siteURL = "{{.Header.Site.URL}}";
var maxRequestSize = "{{.Header.Site.MaxRequestSize}}";
</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" />
{{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>
<body>
{{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}}
@ -53,6 +56,6 @@
<div class="midLeft"></div>
<div id="back" class="zone_{{.Header.Zone}}{{if .Header.Widgets.RightSidebar}} shrink_main{{end}}">
<div id="main" >
<div class="alertbox">{{range .Header.NoticeList}}
<div class="alertbox initial_alertbox">{{range .Header.NoticeList}}
{{template "notice.html" . }}{{end}}
</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;
}
.colstack_item .formrow {
/*.colstack_item .formrow {
display: flex;
}
.colstack_right .formrow {
@ -1267,7 +1267,7 @@ textarea {
width: 40%;
margin-right: 12px;
white-space: nowrap;
}
}*/
.formitem:only-child {
width: 100%;
display: flex;
@ -1295,25 +1295,28 @@ textarea {
#create_topic_page .close_form, #create_topic_page .formlabel, #login_page .formlabel {
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;
}
#login_page .formrow:not(:first-child), #register_page .formrow:not(:first-child) {
.formrow:not(:first-child) {
padding-top: 3px;
}
.formrow {
padding: 16px;
}
.formrow:not(:last-child) {
padding-bottom: 4px;
}
#login_page .formrow:not(:last-child) {
padding-bottom: 0px;
}
#login_page .formrow, #register_page .formrow {
padding: 16px;
}
#register_page .formrow:not(:last-child) {
padding-bottom: 4px;
}
#register_page .formlabel {
.formlabel {
display: block;
font-size: 15px;
}
.quick_create_form .formrow {
padding: 0px;
}
#register_page .register_button_row {
padding: 12px !important;
padding-top: 0px !important;

View File

@ -90,8 +90,14 @@ $(document).ready(function(){
// Move the alerts under the first header
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) {
$('.alert').insertAfter(colSel);
} else if (colSelAlt.length > 0) {
$('.alert').insertBefore(colSelAlt);
} else if (colSelAltAlt.length > 0) {
$('.alert').insertBefore(colSelAltAlt);
} else {
$('.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 {
--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 {
display: flex;
flex-direction: row;
background-color: #333333;
border-radius: 3px;
background-color: var(--third-dark-background);
padding-top: 11px;
padding-bottom: 11px;
padding-left: 12px;
@ -93,7 +112,7 @@ li a {
clear: both;
}
#back {
background: #333333;
background: var(--third-dark-background);
padding: 24px;
padding-top: 12px;
clear: both;
@ -107,11 +126,31 @@ li a {
width: 320px;
}
.rowblock:not(.topic_list):not(.rowhead):not(.opthead) .rowitem {
border-radius: 3px;
background-color: #444444;
display: flex;
padding: 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 {
-webkit-margin-before: 0;
@ -146,6 +185,7 @@ h1, h3 {
margin-bottom: 8px;
}
.topic_row {
border-radius: 3px;
background-color: #444444;
display: flex;
}
@ -212,6 +252,35 @@ h1, h3 {
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 {
display: flex;
margin-top: 8px;
@ -219,6 +288,7 @@ h1, h3 {
.pageitem {
font-size: 17px;
border-radius: 3px;
background-color: #444444;
padding: 7px;
margin-right: 6px;
@ -280,7 +350,7 @@ h1, h3 {
@media(min-width: 1010px) {
.container {
background-color: #292929;
background-color: var(--second-dark-background);
}
#back {
width: 1000px;

View File

@ -25,10 +25,16 @@
.colstack_left .colstack_head:not(:first-child) {
margin-top: 8px;
}
.colstack_left .colstack_head a {
color: rgb(231, 231, 231);
}
.rowmenu {
margin-bottom: 2px;
font-size: 17px;
}
.rowmenu a {
color: rgb(170, 170, 170);
}
.colstack_right {
background-color: #444444;
@ -38,3 +44,57 @@
padding-bottom: 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": [
{
"Name":"EQCSS.js",
"Location":"global"
},
{
"Name":"trumbowyg/trumbowyg.min.js",
"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;
}
.formitem input {
input {
background-color: var(--input-background-color);
border: 1px solid var(--input-border-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;
}