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:
parent
2d7f302768
commit
f8f46b3c48
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 A–Z and ten digits 0–9
|
||||
key, err := base32.StdEncoding.DecodeString(strings.ToUpper(secret))
|
||||
if err != nil {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -142,6 +142,11 @@ type EmailListPage struct {
|
|||
Something interface{}
|
||||
}
|
||||
|
||||
type AccountDashPage struct {
|
||||
*Header
|
||||
MFASetup bool
|
||||
}
|
||||
|
||||
type PanelStats struct {
|
||||
Users int
|
||||
Groups int
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{} {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
410
gen_router.go
410
gen_router.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
"users_replies": "rid",
|
||||
"activity_stream": "asid",
|
||||
"pages": "pid",
|
||||
"replies": "rid",
|
||||
"revisions": "reviseID",
|
||||
"users": "uid",
|
||||
"forums": "fid",
|
||||
"users_groups":"gid",
|
||||
"attachments":"attachID",
|
||||
"users_replies":"rid",
|
||||
"menu_items":"miid",
|
||||
"pages":"pid",
|
||||
"polls":"pollID",
|
||||
"activity_stream":"asid",
|
||||
"users_groups_scheduler":"uid",
|
||||
"replies":"rid",
|
||||
"word_filters":"wfid",
|
||||
"menus":"mid",
|
||||
"registration_logs":"rlid",
|
||||
"users":"uid",
|
||||
"users_2fa_keys":"uid",
|
||||
"forums":"fid",
|
||||
"topics":"tid",
|
||||
"revisions":"reviseID",
|
||||
}
|
||||
|
|
327
general_test.go
327
general_test.go
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
21
main.go
|
@ -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")
|
||||
}
|
||||
|
|
169
panel_routes.go
169
panel_routes.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
"use strict"
|
||||
|
||||
$(document).ready(function(){
|
||||
$("#dash_username input").click(function(){
|
||||
$("#dash_username button").show();
|
||||
});
|
||||
});
|
|
@ -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");
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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])
|
||||
);
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
|
|
|
@ -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`)
|
||||
);
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"DBVersion":"5",
|
||||
"DBVersion":"6",
|
||||
"DynamicFileVersion":"0",
|
||||
"MinGoVersion":"1.10",
|
||||
"MinVersion":""
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" . }}
|
|
@ -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" . }}
|
|
@ -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" . }}
|
|
@ -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" . }}
|
|
@ -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>
|
||||
|
|
|
@ -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" . }}
|
|
@ -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>
|
|
@ -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" . }}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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.**
|
|
@ -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
|
|
@ -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 |
|
@ -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 |
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -14,10 +14,6 @@
|
|||
}
|
||||
],
|
||||
"Resources": [
|
||||
{
|
||||
"Name":"EQCSS.js",
|
||||
"Location":"global"
|
||||
},
|
||||
{
|
||||
"Name":"trumbowyg/trumbowyg.min.js",
|
||||
"Location":"global",
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue