Added the Panel Debug page.

Added the mini stats for the various parts of the Control Panel in the Control Panel Menu.
Fixed a crash bug in the router.
The basic arithmetic template functions now work for the interpreted templates.
Tweaked the connection pool to hopefully ease the number of goroutines fighting over a database connection.
The Group Manager is now paginated.
panel.css is now pushed by HTTP/2 Push.
Fixed an issue with the padding on /forum/ and /topics/ for Shadow.
Fixed a bug in the login system forcefully overriding sessions on login.
All admin logins are now logged.
Refactored the client-side alert loading logic.
Added SimpleCount to the Query Builder.
This commit is contained in:
Azareal 2017-08-15 14:47:56 +01:00
parent 0955a6eda2
commit 3e4cfa8888
25 changed files with 480 additions and 93 deletions

View File

@ -6,6 +6,7 @@ import "database/sql"
var db *sql.DB
var db_version string
var db_adapter string
var ErrNoRows = sql.ErrNoRows

View File

@ -6,6 +6,9 @@ import "bytes"
import "net/http"
import "runtime/debug"
//var notfound_count_per_second int
//var noperms_count_per_second int
var error_internal []byte
var error_notfound []byte
func init_errors() error {

View File

@ -35,6 +35,8 @@ type ForumStore interface
//GetChildren(parentID int, parentType string) ([]*Forum,error)
//GetFirstChild(parentID int, parentType string) (*Forum,error)
CreateForum(forum_name string, forum_desc string, active bool, preset string) (int, error)
GetGlobalCount() int
}
type StaticForumStore struct
@ -45,6 +47,7 @@ type StaticForumStore struct
get *sql.Stmt
get_all *sql.Stmt
forum_count *sql.Stmt
}
func NewStaticForumStore() *StaticForumStore {
@ -56,9 +59,14 @@ func NewStaticForumStore() *StaticForumStore {
if err != nil {
log.Fatal(err)
}
forum_count_stmt, err := qgen.Builder.SimpleCount("forums","name != ''","")
if err != nil {
log.Fatal(err)
}
return &StaticForumStore{
get: get_stmt,
get_all: get_all_stmt,
forum_count: forum_count_stmt,
}
}
@ -322,6 +330,17 @@ func (sfs *StaticForumStore) fill_forum_id_gap(biggerID int, smallerID int) {
}
}
// Return the total number of forums
// TO-DO: Get the total count of forums in the forum store minus the blanked forums rather than doing a heavy query for this?
func (sfs *StaticForumStore) GetGlobalCount() int {
var fcount int
err := sfs.forum_count.QueryRow().Scan(&fcount)
if err != nil {
LogError(err)
}
return fcount
}
// TO-DO: Work on MapForumStore
// TO-DO: Work on SqlForumStore

View File

@ -108,6 +108,7 @@ var delete_topic_stmt *sql.Stmt
var delete_profile_reply_stmt *sql.Stmt
var delete_forum_perms_by_forum_stmt *sql.Stmt
var report_exists_stmt *sql.Stmt
var group_count_stmt *sql.Stmt
var add_forum_perms_to_forum_admins_stmt *sql.Stmt
var add_forum_perms_to_forum_staff_stmt *sql.Stmt
var add_forum_perms_to_forum_members_stmt *sql.Stmt
@ -724,6 +725,12 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing group_count statement.")
group_count_stmt, err = db.Prepare("SELECT COUNT(*) AS `count` FROM `users_groups`")
if err != nil {
return err
}
log.Print("Preparing add_forum_perms_to_forum_admins statement.")
add_forum_perms_to_forum_admins_stmt, err = db.Prepare("INSERT INTO `forums_permissions`(`gid`,`fid`,`preset`,`permissions`) SELECT `gid`, ? AS `fid`, ? AS `preset`, ? AS `permissions` FROM `users_groups` WHERE `is_admin` = 1")
if err != nil {

View File

@ -50,7 +50,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// default_route(w,req)
// return
//}
if req.URL.Path[0] != '/' {
if len(req.URL.Path) == 0 || req.URL.Path[0] != '/' {
w.WriteHeader(405)
w.Write([]byte(""))
return
@ -198,6 +198,9 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
case "/panel/logs/mod/":
route_panel_logs_mod(w,req,user)
return
case "/panel/debug/":
route_panel_debug(w,req,user)
return
default:
route_panel(w,req,user)
return

View File

@ -145,6 +145,7 @@ site.Url = "` + site_url + `"
site.Port = "` + server_port + `"
site.EnableSsl = false
site.EnableEmails = false
site.HasProxy = false // Cloudflare counts as this, if it's sitting in the middle
config.SslPrivkey = ""
config.SslFullchain = ""

61
main.go
View File

@ -210,15 +210,59 @@ func init_templates() {
}
compile_templates()
// Filler functions for now...
filler_func := func(in interface{}, in2 interface{})interface{} {
return 1
}
// TO-DO: Add support for 64-bit integers
// TO-DO: Add support for floats
fmap := make(map[string]interface{})
fmap["add"] = filler_func
fmap["subtract"] = filler_func
fmap["multiply"] = filler_func
fmap["divide"] = filler_func
fmap["add"] = func(left interface{}, right interface{})interface{} {
var left_int int
var right_int int
switch left := left.(type) {
case uint, uint8, uint16, int, int32: left_int = left.(int)
}
switch right := right.(type) {
case uint, uint8, uint16, int, int32: right_int = right.(int)
}
return left_int + right_int
}
fmap["subtract"] = func(left interface{}, right interface{})interface{} {
var left_int int
var right_int int
switch left := left.(type) {
case uint, uint8, uint16, int, int32: left_int = left.(int)
}
switch right := right.(type) {
case uint, uint8, uint16, int, int32: right_int = right.(int)
}
return left_int - right_int
}
fmap["multiply"] = func(left interface{}, right interface{})interface{} {
var left_int int
var right_int int
switch left := left.(type) {
case uint, uint8, uint16, int, int32: left_int = left.(int)
}
switch right := right.(type) {
case uint, uint8, uint16, int, int32: right_int = right.(int)
}
return left_int * right_int
}
fmap["divide"] = func(left interface{}, right interface{})interface{} {
var left_int int
var right_int int
switch left := left.(type) {
case uint, uint8, uint16, int, int32: left_int = left.(int)
}
switch right := right.(type) {
case uint, uint8, uint16, int, int32: right_int = right.(int)
}
if left_int == 0 || right_int == 0 {
return 0
}
return left_int / right_int
}
// The interpreted templates...
if dev.DebugMode {
@ -377,6 +421,7 @@ func main(){
///router.HandleFunc("/panel/groups/edit/perms/submit/", route_panel_groups_edit_perms_submit)
///router.HandleFunc("/panel/groups/create/", route_panel_groups_create_submit)
///router.HandleFunc("/panel/logs/mod/", route_panel_logs_mod)
///router.HandleFunc("/panel/debug/", route_panel_debug)
///router.HandleFunc("/api/", route_api)
//router.HandleFunc("/exit/", route_exit)

View File

@ -4,6 +4,7 @@
package main
import "log"
//import "time"
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
import "./query_gen/lib"
@ -16,6 +17,10 @@ var todays_topic_count_stmt *sql.Stmt
var todays_report_count_stmt *sql.Stmt
var todays_newuser_count_stmt *sql.Stmt
func init() {
db_adapter = "mysql"
}
func _init_database() (err error) {
var _dbpassword string
if(db_config.Password != ""){
@ -39,6 +44,10 @@ func _init_database() (err error) {
// Set the number of max open connections
db.SetMaxOpenConns(64)
db.SetMaxIdleConns(32)
// Only hold connections open for five seconds to avoid accumulating a large number of stale connections
//db.SetConnMaxLifetime(5 * time.Second)
// Build the generated prepared statements, we are going to slowly move the queries over to the query generator rather than writing them all by hand, this'll make it easier for us to implement database adapters for other databases like PostgreSQL, MSSQL, SQlite, etc.
err = _gen_mysql()

View File

@ -106,6 +106,39 @@ type CreateTopicPage struct
ExtData ExtData
}
type PanelStats struct
{
Users int
Groups int
Forums int
Settings int
Themes int
Reports int
}
type PanelPage struct
{
Title string
CurrentUser User
Header HeaderVars
Stats PanelStats
ItemList []interface{}
Something interface{}
}
type PanelGroupPage struct
{
Title string
CurrentUser User
Header HeaderVars
Stats PanelStats
ItemList []GroupAdmin
PageList []int
Page int
LastPage int
ExtData ExtData
}
type GridElement struct
{
ID string
@ -122,25 +155,28 @@ type PanelDashboardPage struct
Title string
CurrentUser User
Header HeaderVars
Stats PanelStats
GridItems []GridElement
ExtData ExtData
}
type ThemesPage struct
type PanelThemesPage struct
{
Title string
CurrentUser User
Header HeaderVars
Stats PanelStats
PrimaryThemes []Theme
VariantThemes []Theme
ExtData ExtData
}
type EditGroupPage struct
type PanelEditGroupPage struct
{
Title string
CurrentUser User
Header HeaderVars
Stats PanelStats
ID int
Name string
Tag string
@ -155,11 +191,12 @@ type GroupForumPermPreset struct
Preset string
}
type EditForumPage struct
type PanelEditForumPage struct
{
Title string
CurrentUser User
Header HeaderVars
Stats PanelStats
ID int
Name string
Desc string
@ -182,11 +219,12 @@ type NameLangToggle struct
Toggle bool
}
type EditGroupPermsPage struct
type PanelEditGroupPermsPage struct
{
Title string
CurrentUser User
Header HeaderVars
Stats PanelStats
ID int
Name string
LocalPerms []NameLangToggle
@ -200,15 +238,28 @@ type Log struct {
DoneAt string
}
type LogsPage struct
type PanelLogsPage struct
{
Title string
CurrentUser User
Header HeaderVars
Stats PanelStats
Logs []Log
ExtData ExtData
}
type PanelDebugPage struct
{
Title string
CurrentUser User
Header HeaderVars
Stats PanelStats
Uptime string
OpenConns int
DBAdapter string
ExtData ExtData
}
type PageSimple struct
{
Title string
@ -747,3 +798,41 @@ func coerce_int_bytes(data []byte) (res int, length int) {
}
return conv, i
}
// TO-DO: Write tests for this
func paginate(count int, per_page int, maxPages int) []int {
if count < per_page {
return []int{1}
}
var page int
var out []int
for current := 0; current < count; current += per_page {
page++
out = append(out,page)
if len(out) >= maxPages {
break
}
}
return out
}
// TO-DO: Write tests for this
func page_offset(count int, page int, perPage int) (int, int, int) {
var offset int
lastPage := int(count / perPage) + 1
if page > 1 {
offset = (perPage * page) - perPage
} else if page == -1 {
page = lastPage
offset = (perPage * page) - perPage
} else {
page = 1
}
// We don't want the offset to overflow the slices, if everything's in memory
if offset >= (count - 1) {
offset = 0
}
return offset, page, lastPage
}

View File

@ -15,7 +15,7 @@ import (
)
func route_panel(w http.ResponseWriter, r *http.Request, user User){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -168,7 +168,7 @@ func route_panel(w http.ResponseWriter, r *http.Request, user User){
gridElements = append(gridElements, GridElement{"dash-visitorsperweek","2 visitors / week",13,"grid_stat stat_disabled","","","Coming Soon!"/*"The number of unique visitors we've had over the last 7 days"*/})
gridElements = append(gridElements, GridElement{"dash-postsperuser","5 posts / user / week",14,"grid_stat stat_disabled","","","Coming Soon!"/*"The average number of posts made by each active user over the past week"*/})
pi := PanelDashboardPage{"Control Panel Dashboard",user,headerVars,gridElements,extData}
pi := PanelDashboardPage{"Control Panel Dashboard",user,headerVars,stats,gridElements,extData}
if pre_render_hooks["pre_render_panel_dashboard"] != nil {
if run_pre_render_hook("pre_render_panel_dashboard", w, r, &user, &pi) {
return
@ -181,7 +181,7 @@ func route_panel(w http.ResponseWriter, r *http.Request, user User){
}
func route_panel_forums(w http.ResponseWriter, r *http.Request, user User){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -206,7 +206,7 @@ func route_panel_forums(w http.ResponseWriter, r *http.Request, user User){
forumList = append(forumList,fadmin)
}
}
pi := Page{"Forum Manager",user,headerVars,forumList,nil}
pi := PanelPage{"Forum Manager",user,headerVars,stats,forumList,nil}
if pre_render_hooks["pre_render_panel_forums"] != nil {
if run_pre_render_hook("pre_render_panel_forums", w, r, &user, &pi) {
return
@ -254,7 +254,7 @@ func route_panel_forums_create_submit(w http.ResponseWriter, r *http.Request, us
}
func route_panel_forums_delete(w http.ResponseWriter, r *http.Request, user User, sfid string){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -285,7 +285,7 @@ func route_panel_forums_delete(w http.ResponseWriter, r *http.Request, user User
confirm_msg := "Are you sure you want to delete the '" + forum.Name + "' forum?"
yousure := AreYouSure{"/panel/forums/delete/submit/" + strconv.Itoa(fid),confirm_msg}
pi := Page{"Delete Forum",user,headerVars,tList,yousure}
pi := PanelPage{"Delete Forum",user,headerVars,stats,tList,yousure}
if pre_render_hooks["pre_render_panel_delete_forum"] != nil {
if run_pre_render_hook("pre_render_panel_delete_forum", w, r, &user, &pi) {
return
@ -330,7 +330,7 @@ func route_panel_forums_delete_submit(w http.ResponseWriter, r *http.Request, us
}
func route_panel_forums_edit(w http.ResponseWriter, r *http.Request, user User, sfid string) {
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -367,7 +367,7 @@ func route_panel_forums_edit(w http.ResponseWriter, r *http.Request, user User,
gplist = append(gplist,GroupForumPermPreset{group,forum_perms_to_group_forum_preset(group.Forums[fid])})
}
pi := EditForumPage{"Forum Editor",user,headerVars,forum.ID,forum.Name,forum.Desc,forum.Active,forum.Preset,gplist,extData}
pi := PanelEditForumPage{"Forum Editor",user,headerVars,stats,forum.ID,forum.Name,forum.Desc,forum.Active,forum.Preset,gplist,extData}
if pre_render_hooks["pre_render_panel_edit_forum"] != nil {
if run_pre_render_hook("pre_render_panel_edit_forum", w, r, &user, &pi) {
return
@ -548,7 +548,7 @@ func route_panel_forums_edit_perms_submit(w http.ResponseWriter, r *http.Request
}
func route_panel_settings(w http.ResponseWriter, r *http.Request, user User){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -597,7 +597,7 @@ func route_panel_settings(w http.ResponseWriter, r *http.Request, user User){
return
}
pi := Page{"Setting Manager",user,headerVars,tList,settingList}
pi := PanelPage{"Setting Manager",user,headerVars,stats,tList,settingList}
if pre_render_hooks["pre_render_panel_settings"] != nil {
if run_pre_render_hook("pre_render_panel_settings", w, r, &user, &pi) {
return
@ -610,7 +610,7 @@ func route_panel_settings(w http.ResponseWriter, r *http.Request, user User){
}
func route_panel_setting(w http.ResponseWriter, r *http.Request, user User, sname string){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -653,7 +653,7 @@ func route_panel_setting(w http.ResponseWriter, r *http.Request, user User, snam
}
}
pi := Page{"Edit Setting",user,headerVars,itemList,setting}
pi := PanelPage{"Edit Setting",user,headerVars,stats,itemList,setting}
if pre_render_hooks["pre_render_panel_setting"] != nil {
if run_pre_render_hook("pre_render_panel_setting", w, r, &user, &pi) {
return
@ -720,7 +720,7 @@ func route_panel_setting_edit(w http.ResponseWriter, r *http.Request, user User,
}
func route_panel_plugins(w http.ResponseWriter, r *http.Request, user User){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -736,7 +736,7 @@ func route_panel_plugins(w http.ResponseWriter, r *http.Request, user User){
pluginList = append(pluginList,plugin)
}
pi := Page{"Plugin Manager",user,headerVars,pluginList,nil}
pi := PanelPage{"Plugin Manager",user,headerVars,stats,pluginList,nil}
if pre_render_hooks["pre_render_panel_plugins"] != nil {
if run_pre_render_hook("pre_render_panel_plugins", w, r, &user, &pi) {
return
@ -926,7 +926,6 @@ func route_panel_plugins_install(w http.ResponseWriter, r *http.Request, user Us
}
}
if has_plugin {
_, err = update_plugin_install_stmt.Exec(1,uname)
if err != nil {
@ -960,7 +959,7 @@ func route_panel_plugins_install(w http.ResponseWriter, r *http.Request, user Us
}
func route_panel_users(w http.ResponseWriter, r *http.Request, user User){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -1004,7 +1003,7 @@ func route_panel_users(w http.ResponseWriter, r *http.Request, user User){
return
}
pi := Page{"User Manager",user,headerVars,userList,nil}
pi := PanelPage{"User Manager",user,headerVars,stats,userList,nil}
if pre_render_hooks["pre_render_panel_users"] != nil {
if run_pre_render_hook("pre_render_panel_users", w, r, &user, &pi) {
return
@ -1017,7 +1016,7 @@ func route_panel_users(w http.ResponseWriter, r *http.Request, user User){
}
func route_panel_users_edit(w http.ResponseWriter, r *http.Request, user User, suid string){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -1058,7 +1057,7 @@ func route_panel_users_edit(w http.ResponseWriter, r *http.Request, user User, s
groupList = append(groupList,group)
}
pi := Page{"User Editor",user,headerVars,groupList,targetUser}
pi := PanelPage{"User Editor",user,headerVars,stats,groupList,targetUser}
if pre_render_hooks["pre_render_panel_edit_user"] != nil {
if run_pre_render_hook("pre_render_panel_edit_user", w, r, &user, &pi) {
return
@ -1166,13 +1165,25 @@ func route_panel_users_edit_submit(w http.ResponseWriter, r *http.Request, user
}
func route_panel_groups(w http.ResponseWriter, r *http.Request, user User){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
var groupList []interface{}
for _, group := range groups[1:] {
page, _ := strconv.Atoi(r.FormValue("page"))
perPage := 9
offset, page, lastPage := page_offset(stats.Groups, page, perPage)
// Skip the System group
offset++
var count int
var groupList []GroupAdmin
for _, group := range groups[offset:] {
if count == perPage {
break
}
var rank string
var rank_class string
var can_edit bool
@ -1197,10 +1208,12 @@ func route_panel_groups(w http.ResponseWriter, r *http.Request, user User){
can_edit = user.Perms.EditGroup && (!group.Is_Admin || user.Perms.EditGroupAdmin) && (!group.Is_Mod || user.Perms.EditGroupSuperMod)
groupList = append(groupList, GroupAdmin{group.ID,group.Name,rank,rank_class,can_edit,can_delete})
count++
}
//log.Printf("groupList: %+v\n", groupList)
pi := Page{"Group Manager",user,headerVars,groupList,nil}
pageList := paginate(stats.Groups, perPage, 5)
pi := PanelGroupPage{"Group Manager",user,headerVars,stats,groupList,pageList,page,lastPage,extData}
if pre_render_hooks["pre_render_panel_groups"] != nil {
if run_pre_render_hook("pre_render_panel_groups", w, r, &user, &pi) {
return
@ -1214,7 +1227,7 @@ func route_panel_groups(w http.ResponseWriter, r *http.Request, user User){
}
func route_panel_groups_edit(w http.ResponseWriter, r *http.Request, user User, sgid string){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -1260,7 +1273,7 @@ func route_panel_groups_edit(w http.ResponseWriter, r *http.Request, user User,
disable_rank := !user.Perms.EditGroupGlobalPerms || (group.ID == 6)
pi := EditGroupPage{"Group Editor",user,headerVars,group.ID,group.Name,group.Tag,rank,disable_rank,extData}
pi := PanelEditGroupPage{"Group Editor",user,headerVars,stats,group.ID,group.Name,group.Tag,rank,disable_rank,extData}
if pre_render_hooks["pre_render_panel_edit_group"] != nil {
if run_pre_render_hook("pre_render_panel_edit_group", w, r, &user, &pi) {
return
@ -1273,7 +1286,7 @@ func route_panel_groups_edit(w http.ResponseWriter, r *http.Request, user User,
}
func route_panel_groups_edit_perms(w http.ResponseWriter, r *http.Request, user User, sgid string){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -1338,7 +1351,7 @@ func route_panel_groups_edit_perms(w http.ResponseWriter, r *http.Request, user
globalPerms = append(globalPerms, NameLangToggle{"ViewAdminLogs",GetGlobalPermPhrase("ViewAdminLogs"),group.Perms.ViewAdminLogs})
globalPerms = append(globalPerms, NameLangToggle{"ViewIPs",GetGlobalPermPhrase("ViewIPs"),group.Perms.ViewIPs})
pi := EditGroupPermsPage{"Group Editor",user,headerVars,group.ID,group.Name,localPerms,globalPerms,extData}
pi := PanelEditGroupPermsPage{"Group Editor",user,headerVars,stats,group.ID,group.Name,localPerms,globalPerms,extData}
if pre_render_hooks["pre_render_panel_edit_group_perms"] != nil {
if run_pre_render_hook("pre_render_panel_edit_group_perms", w, r, &user, &pi) {
return
@ -1609,7 +1622,7 @@ func route_panel_groups_create_submit(w http.ResponseWriter, r *http.Request, us
}
func route_panel_themes(w http.ResponseWriter, r *http.Request, user User){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -1631,7 +1644,7 @@ func route_panel_themes(w http.ResponseWriter, r *http.Request, user User){
}
pi := ThemesPage{"Theme Manager",user,headerVars,pThemeList,vThemeList,extData}
pi := PanelThemesPage{"Theme Manager",user,headerVars,stats,pThemeList,vThemeList,extData}
if pre_render_hooks["pre_render_panel_themes"] != nil {
if run_pre_render_hook("pre_render_panel_themes", w, r, &user, &pi) {
return
@ -1722,7 +1735,7 @@ func route_panel_themes_default(w http.ResponseWriter, r *http.Request, user Use
}
func route_panel_logs_mod(w http.ResponseWriter, r *http.Request, user User){
headerVars, ok := PanelSessionCheck(w,r,&user)
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
@ -1813,7 +1826,7 @@ func route_panel_logs_mod(w http.ResponseWriter, r *http.Request, user User){
return
}
pi := LogsPage{"Moderation Logs",user,headerVars,logs,extData}
pi := PanelLogsPage{"Moderation Logs",user,headerVars,stats,logs,extData}
if pre_render_hooks["pre_render_panel_mod_log"] != nil {
if run_pre_render_hook("pre_render_panel_mod_log", w, r, &user, &pi) {
return
@ -1821,6 +1834,28 @@ func route_panel_logs_mod(w http.ResponseWriter, r *http.Request, user User){
}
err = templates.ExecuteTemplate(w,"panel-modlogs.html",pi)
if err != nil {
log.Print(err)
InternalError(err,w)
}
}
func route_panel_debug(w http.ResponseWriter, r *http.Request, user User) {
headerVars, stats, ok := PanelSessionCheck(w,r,&user)
if !ok {
return
}
if !user.Is_Admin {
NoPermissions(w,r,user)
return
}
uptime := "..."
db_stats := db.Stats()
open_conn_count := db_stats.OpenConnections
// Disk I/O?
pi := PanelDebugPage{"Debug",user,headerVars,stats,uptime,open_conn_count,db_adapter,extData}
err := templates.ExecuteTemplate(w,"panel-debug.html",pi)
if err != nil {
InternalError(err,w)
}
}

View File

@ -5,6 +5,7 @@
package main
import "strings"
//import "time"
import "database/sql"
import _ "github.com/lib/pq"
import "./query_gen/lib"
@ -18,6 +19,10 @@ var todays_topic_count_stmt *sql.Stmt
var todays_report_count_stmt *sql.Stmt
var todays_newuser_count_stmt *sql.Stmt
func init() {
db_adapter = "pgsql"
}
func _init_database() (err error) {
// TO-DO: Investigate connect_timeout to see what it does exactly and whether it's relevant to us
var _dbpassword string
@ -40,6 +45,10 @@ func _init_database() (err error) {
// Set the number of max open connections. How many do we need? Might need to do some tests.
db.SetMaxOpenConns(64)
db.SetMaxIdleConns(32)
// Only hold connections open for five seconds to avoid accumulating a large number of stale connections
//db.SetConnMaxLifetime(5 * time.Second)
err = _gen_pgsql()
if err != nil {

View File

@ -13,14 +13,16 @@ function post_link(event)
function load_alerts(menu_alerts)
{
menu_alerts.find(".alert_counter").text("");
var alertListNode = menu_alerts.getElementsByClassName("alertList")[0];
var alertCounterNode = menu_alerts.getElementsByClassName("alert_counter")[0];
alertCounterNode.textContent = "";
$.ajax({
type: 'get',
dataType: 'json',
url:'/api/?action=get&module=alerts&format=json',
success: function(data) {
if("errmsg" in data) {
menu_alerts.find(".alertList").html("<div class='alertItem'>"+data.errmsg+"</div>");
alertListNode.innerHTML = "<div class='alertItem'>"+data.errmsg+"</div>";
return;
}
@ -55,8 +57,13 @@ function load_alerts(menu_alerts)
//menu_alerts.removeClass("hasAvatars");
//if(anyAvatar) menu_alerts.addClass("hasAvatars");
}
menu_alerts.find(".alertList").html(alist);
if(data.msgCount != 0) menu_alerts.find(".alert_counter").text(data.msgCount);
alertListNode.innerHTML = alist;
if(data.msgCount != 0) {
alertCounterNode.textContent = data.msgCount;
menu_alerts.classList.add("has_alerts");
} else {
menu_alerts.classList.remove("has_alerts");
}
alertCount = data.msgCount;
},
error: function(magic,theStatus,error) {
@ -69,36 +76,36 @@ function load_alerts(menu_alerts)
console.log(magic.responseText);
console.log(err);
}
menu_alerts.find(".alertList").html("<div class='alertItem'>"+errtxt+"</div>");
alertListNode.innerHTML = "<div class='alertItem'>"+errtxt+"</div>";
}
});
}
$(document).ready(function(){
function SplitN(data,ch,n) {
var out = [];
if(data.length == 0) return out;
function SplitN(data,ch,n) {
var out = [];
if(data.length == 0) return out;
var lastIndex = 0;
var j = 0;
var lastN = 1;
for(var i = 0; i < data.length; i++) {
if(data[i] == ch) {
out[j++] = data.substring(lastIndex,i);
lastIndex = i;
if(lastN == n) break;
lastN++;
}
var lastIndex = 0;
var j = 0;
var lastN = 1;
for(var i = 0; i < data.length; i++) {
if(data[i] == ch) {
out[j++] = data.substring(lastIndex,i);
lastIndex = i;
if(lastN == n) break;
lastN++;
}
if(data.length > lastIndex) out[out.length - 1] += data.substring(lastIndex);
return out;
}
if(data.length > lastIndex) out[out.length - 1] += data.substring(lastIndex);
return out;
}
$(document).ready(function(){
if(window["WebSocket"]) {
if(window.location.protocol == "https:")
conn = new WebSocket("wss://" + document.location.host + "/ws/");
else conn = new WebSocket("ws://" + document.location.host + "/ws/");
conn.onopen = function() {
conn.send("page " + document.location.pathname + '\r');
}
@ -342,14 +349,15 @@ $(document).ready(function(){
}
});
$(".menu_alerts").ready(function(){
load_alerts($(this));
});
var alert_menu_list = document.getElementsByClassName("menu_alerts");
for(var i = 0; i < alert_menu_list.length; i++) {
load_alerts(alert_menu_list[i]);
}
$(".menu_alerts").click(function(event) {
event.stopPropagation();
if($(this).hasClass("selectedAlert")) return;
if(!conn) load_alerts($(this));
if(!conn) load_alerts(this);
this.className += " selectedAlert";
document.getElementById("back").className += " alertActive"
});

View File

@ -1,7 +1,7 @@
/* WIP Under Construction */
package qgen
//import "fmt"
//import "log"
import "database/sql"
var Builder *builder
@ -38,12 +38,21 @@ func (build *builder) SimpleSelect(table string, columns string, where string, o
return build.conn.Prepare(res)
}
func (build *builder) SimpleCount(table string, where string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleCount("_builder", table, where, limit)
if err != nil {
return stmt, err
}
//log.Print("res",res)
return build.conn.Prepare(res)
}
func (build *builder) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)
if err != nil {
return stmt, err
}
//fmt.Println("res",res)
//log.Print("res",res)
return build.conn.Prepare(res)
}
@ -52,7 +61,7 @@ func (build *builder) SimpleInnerJoin(table1 string, table2 string, columns stri
if err != nil {
return stmt, err
}
//fmt.Println("res",res)
//log.Print("res",res)
return build.conn.Prepare(res)
}

View File

@ -393,6 +393,8 @@ func write_deletes(adapter qgen.DB_Adapter) error {
func write_simple_counts(adapter qgen.DB_Adapter) error {
adapter.SimpleCount("report_exists","topics","data = ? AND data != '' AND parentID = 1","")
adapter.SimpleCount("group_count","users_groups","","")
return nil
}

View File

@ -31,7 +31,7 @@ func (router *Router) HandleFunc(pattern string, handle func(http.ResponseWriter
}
func (router *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if req.URL.Path[0] != '/' {
if len(req.URL.Path) == 0 || req.URL.Path[0] != '/' {
w.WriteHeader(405)
w.Write([]byte(""))
return

View File

@ -127,7 +127,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// default_route(w,req)
// return
//}
if req.URL.Path[0] != '/' {
if len(req.URL.Path) == 0 || req.URL.Path[0] != '/' {
w.WriteHeader(405)
w.Write([]byte(""))
return

View File

@ -75,5 +75,6 @@ func routes() {
Route{"route_panel_groups_create_submit","/panel/groups/create/","",[]string{}},
Route{"route_panel_logs_mod","/panel/logs/mod/","",[]string{}},
Route{"route_panel_debug","/panel/debug/","",[]string{}},
)
}

View File

@ -62,6 +62,9 @@ func route_static(w http.ResponseWriter, r *http.Request){
if strings.Contains(r.Header.Get("Accept-Encoding"),"gzip") {
h.Set("Content-Encoding","gzip")
h.Set("Content-Length", strconv.FormatInt(file.GzipLength, 10))
if site.HasProxy {
h.Set("Vary","Accept-Encoding")
}
io.Copy(w, bytes.NewReader(file.GzipData)) // Use w.Write instead?
} else {
h.Set("Content-Length", strconv.FormatInt(file.Length, 10)) // Avoid doing a type conversion every time?
@ -139,6 +142,7 @@ func route_custom_page(w http.ResponseWriter, r *http.Request, user User){
}
}
// TO-DO: Paginate this
func route_topics(w http.ResponseWriter, r *http.Request, user User){
headerVars, ok := SessionCheck(w,r,&user)
if !ok {
@ -1726,6 +1730,9 @@ func route_login(w http.ResponseWriter, r *http.Request, user User) {
templates.ExecuteTemplate(w,"login.html",pi)
}
// TO-DO: Log failed attempted logins?
// TO-DO: Lock IPS out if they have too many failed attempts?
// TO-DO: Log unusual countries in comparison to the country a user usually logs in from? Alert the user about this?
func route_login_submit(w http.ResponseWriter, r *http.Request, user User) {
if user.Loggedin {
LocalError("You're already logged in.",w,r,user)
@ -1743,6 +1750,13 @@ func route_login_submit(w http.ResponseWriter, r *http.Request, user User) {
return
}
userPtr, err := users.CascadeGet(uid)
if err != nil {
LocalError("Bad account",w,r,user)
return
}
user = *userPtr
var session string
if user.Session == "" {
session, err = auth.CreateSession(uid)
@ -1755,6 +1769,15 @@ func route_login_submit(w http.ResponseWriter, r *http.Request, user User) {
}
auth.SetCookies(w,uid,session)
if user.Is_Admin {
// Is this error check reundant? We already check for the error in PreRoute for the same IP
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
InternalError(err,w)
return
}
log.Print("#" + strconv.Itoa(uid) + " has logged in with IP " + host)
}
http.Redirect(w,r,"/",http.StatusSeeOther)
}

View File

@ -15,6 +15,7 @@ type Site struct
Port string
EnableSsl bool
EnableEmails bool
HasProxy bool
}
type DB_Config struct

View File

@ -0,0 +1,17 @@
{{template "header.html" . }}
{{template "panel-menu.html" . }}
<div id="panel_dashboard_right" class="colstack_right">
<div class="colstack_item colstack_head">
<div class="rowitem"><a>Debug</a></div>
</div>
<div id="panel_debug" class="colstack_grid">
<div class="grid_item grid_stat">Uptime</div>
<div class="grid_item grid_stat">Open DB Conns</div>
<div class="grid_item grid_stat">Adapter</div>
<div class="grid_item grid_stat">{{.Uptime}}</div>
<div class="grid_item grid_stat">{{.OpenConns}}</div>
<div class="grid_item grid_stat">{{.DBAdapter}}</div>
</div>
</div>
{{template "footer.html" . }}

View File

@ -18,6 +18,15 @@
</div>
{{end}}
</div>
{{if gt .LastPage 1}}
<div class="pageset">
{{if gt .Page 1}}<div class="pageitem"><a href="?page={{subtract .Page 1}}">Prev</a></div>{{end}}
{{range .PageList}}
<div class="pageitem"><a href="?page={{.}}">{{.}}</a></div>
{{end}}
{{if ne .LastPage .Page}}<div class="pageitem"><a href="?page={{add .Page 1}}">Next</a></div>{{end}}
</div>
{{end}}
<div class="colstack_item colstack_head">
<div class="rowitem"><a>Create Group</a></div>
</div>

View File

@ -2,12 +2,36 @@
<div class="rowitem"><a href="/panel/">Control Panel</a></div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/panel/users/">Users</a></div>
<div class="rowitem passive"><a href="/panel/groups/">Groups</a></div>
{{if .CurrentUser.Perms.ManageForums}}<div class="rowitem passive"><a href="/panel/forums/">Forums</a></div>{{end}}
{{if .CurrentUser.Perms.EditSettings}}<div class="rowitem passive"><a href="/panel/settings/">Settings</a></div>{{end}}
{{if .CurrentUser.Perms.ManageThemes}}<div class="rowitem passive"><a href="/panel/themes/">Themes</a></div>{{end}}
{{if .CurrentUser.Perms.ManagePlugins}}<div class="rowitem passive"><a href="/panel/plugins/">Plugins</a></div>{{end}}
<div class="rowitem passive"><a href="/forum/1">Reports</a></div>
<div class="rowitem passive"><a href="/panel/logs/mod/">Logs</a></div>
<div class="rowitem passive">
<a href="/panel/users/">Users</a> <a class="menu_stats" href="#">({{.Stats.Users}})</a>
</div>
<div class="rowitem passive">
<a href="/panel/groups/">Groups</a> <a class="menu_stats" href="#">({{.Stats.Groups}})</a>
</div>
{{if .CurrentUser.Perms.ManageForums}}<div class="rowitem passive">
<a href="/panel/forums/">Forums</a> <a class="menu_stats" href="#">({{.Stats.Forums}})</a>
</div>{{end}}
{{if .CurrentUser.Perms.EditSettings}}<div class="rowitem passive">
<a href="/panel/settings/">Settings</a> <a class="menu_stats" href="#">({{.Stats.Settings}})</a>
</div>{{end}}
{{if .CurrentUser.Perms.ManageThemes}}<div class="rowitem passive">
<a href="/panel/themes/">Themes</a> <a class="menu_stats" href="#">({{.Stats.Themes}})</a>
</div>{{end}}
<div class="rowitem passive">
<a href="/forum/1">Reports</a> <a class="menu_stats" href="#">({{.Stats.Reports}})</a>
</div>
</div>
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="#">System</a></div>
</div>
<div class="colstack_item rowmenu">
{{if .CurrentUser.Perms.ManagePlugins}}<div class="rowitem passive">
<a href="/panel/plugins/">Plugins</a>
</div>{{end}}
<div class="rowitem passive">
<a href="/panel/logs/mod/">Logs</a>
</div>
{{if .CurrentUser.Is_Admin}}<div class="rowitem passive">
<a href="/panel/debug/">Debug</a>
</div>{{end}}
</div>

View File

@ -330,6 +330,23 @@ textarea.large {
font-size: 13px;
padding: 10px;
}
.menu_stats {
font-size: 12px;
}
/* Mini paginators aka panel paginators */
.pageset {
margin-top: 4px;
clear: both;
height: 32px;
}
.pageitem {
background-color: rgb(61,61,61);
padding: 10px;
margin-right: 4px;
font-size: 13px;
float: left;
}
.rowlist.bgavatars .rowitem {
background-repeat: no-repeat;
@ -429,6 +446,9 @@ input, select, textarea {
margin-left: 8px;
width: 108px;
}
.topic_list .rowitem:last-child {
margin-bottom: 10px;
}
/* Profiles */
#profile_left_lane {

21
user.go
View File

@ -14,7 +14,7 @@ import (
var guest_user User = User{ID:0,Link:"#",Group:6,Perms:GuestPerms}
var PreRoute func(http.ResponseWriter, *http.Request) (User,bool) = _pre_route
var PanelSessionCheck func(http.ResponseWriter, *http.Request, *User) (HeaderVars,bool) = _panel_session_check
var PanelSessionCheck func(http.ResponseWriter, *http.Request, *User) (HeaderVars,PanelStats,bool) = _panel_session_check
var SimplePanelSessionCheck func(http.ResponseWriter, *http.Request, *User) bool = _simple_panel_session_check
var SimpleForumSessionCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (success bool) = _simple_forum_session_check
var ForumSessionCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (headerVars HeaderVars, success bool) = _forum_session_check
@ -208,11 +208,11 @@ func _forum_session_check(w http.ResponseWriter, r *http.Request, user *User, fi
}
// Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with
func _panel_session_check(w http.ResponseWriter, r *http.Request, user *User) (headerVars HeaderVars, success bool) {
func _panel_session_check(w http.ResponseWriter, r *http.Request, user *User) (headerVars HeaderVars, stats PanelStats, success bool) {
headerVars.Site = site
if !user.Is_Super_Mod {
NoPermissions(w,r,*user)
return headerVars, false
return headerVars, stats, false
}
headerVars.Stylesheets = append(headerVars.Stylesheets,"panel.css")
@ -233,9 +233,22 @@ func _panel_session_check(w http.ResponseWriter, r *http.Request, user *User) (h
}
}
err := group_count_stmt.QueryRow().Scan(&stats.Groups)
if err != nil {
InternalError(err,w)
return headerVars, stats, false
}
stats.Users = users.GetGlobalCount()
stats.Forums = fstore.GetGlobalCount() // TO-DO: Stop it from showing the blanked forums
stats.Settings = len(settings) // TO-DO: IS this racey?
stats.Themes = len(themes)
stats.Reports = 0 // TO-DO: Do the report count. Only show open threads?
pusher, ok := w.(http.Pusher)
if ok {
pusher.Push("/static/main.css", nil)
pusher.Push("/static/panel.css", nil)
pusher.Push("/static/global.js", nil)
pusher.Push("/static/jquery-3.1.1.min.js", nil)
// TO-DO: Push the theme CSS files
@ -243,7 +256,7 @@ func _panel_session_check(w http.ResponseWriter, r *http.Request, user *User) (h
// TO-DO: Push avatars?
}
return headerVars, true
return headerVars, stats, true
}
func _simple_panel_session_check(w http.ResponseWriter, r *http.Request, user *User) (success bool) {
if !user.Is_Super_Mod {

View File

@ -32,6 +32,7 @@ type UserStore interface {
CreateUser(username string, password string, email string, group int, active int) (int, error)
GetLength() int
GetCapacity() int
GetGlobalCount() int
}
type MemoryUserStore struct {
@ -41,6 +42,7 @@ type MemoryUserStore struct {
get *sql.Stmt
register *sql.Stmt
username_exists *sql.Stmt
user_count *sql.Stmt
sync.RWMutex
}
@ -62,12 +64,18 @@ func NewMemoryUserStore(capacity int) *MemoryUserStore {
log.Fatal(err)
}
user_count_stmt, err := qgen.Builder.SimpleCount("users","","")
if err != nil {
log.Fatal(err)
}
return &MemoryUserStore{
items:make(map[int]*User),
capacity:capacity,
get:get_stmt,
register:register_stmt,
username_exists:username_exists_stmt,
user_count:user_count_stmt,
}
}
@ -352,10 +360,21 @@ func (sus *MemoryUserStore) GetCapacity() int {
return sus.capacity
}
// Return the total number of users registered on the forums
func (sus *MemoryUserStore) GetGlobalCount() int {
var ucount int
err := sus.user_count.QueryRow().Scan(&ucount)
if err != nil {
LogError(err)
}
return ucount
}
type SqlUserStore struct {
get *sql.Stmt
register *sql.Stmt
username_exists *sql.Stmt
user_count *sql.Stmt
}
func NewSqlUserStore() *SqlUserStore {
@ -376,10 +395,16 @@ func NewSqlUserStore() *SqlUserStore {
log.Fatal(err)
}
user_count_stmt, err := qgen.Builder.SimpleCount("users","","")
if err != nil {
log.Fatal(err)
}
return &SqlUserStore{
get:get_stmt,
register:register_stmt,
username_exists:username_exists_stmt,
get: get_stmt,
register: register_stmt,
username_exists: username_exists_stmt,
user_count: user_count_stmt,
}
}
@ -551,6 +576,20 @@ func (sus *SqlUserStore) GetCapacity() int {
return 0
}
// Return the total number of users registered on the forums
func (sus *SqlUserStore) GetLength() int {
return 0 // Return the total number of users registered on the forums?
var ucount int
err := sus.user_count.QueryRow().Scan(&ucount)
if err != nil {
LogError(err)
}
return ucount
}
func (sus *SqlUserStore) GetGlobalCount() int {
var ucount int
err := sus.user_count.QueryRow().Scan(&ucount)
if err != nil {
LogError(err)
}
return ucount
}