1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-19 08:26:23 -07:00

Rework command line staff management commands and password handling, update logging initialization options, and bump version to 4.2.0 in preparation for upcoming release

This commit is contained in:
Eggbertx 2025-05-07 14:51:57 -07:00
parent 305b557e41
commit d923c73000
10 changed files with 307 additions and 129 deletions

217
cmd/gochan/commandline.go Normal file
View file

@ -0,0 +1,217 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"syscall"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gcutil"
"github.com/rs/zerolog"
"golang.org/x/term"
)
func getPassword() (string, error) {
var password string
fd := int(os.Stdin.Fd())
state, err := term.MakeRaw(fd)
if err != nil {
return "", err
}
defer term.Restore(fd, state)
for {
input := make([]byte, 1)
if _, err := syscall.Read(int(fd), input); err != nil {
term.Restore(fd, state)
return "", err
}
if input[0] == '\n' || input[0] == '\r' {
term.Restore(fd, state)
fmt.Println()
break
}
if input[0] == 127 || input[0] == 8 {
if len(password) > 0 {
password = password[:len(password)-1]
fmt.Print("\b \b")
}
} else if input[0] == 3 {
term.Restore(fd, state)
fmt.Println("\nAborted.")
os.Exit(1)
} else {
password += string(input[0])
fmt.Print("*")
}
}
return password, nil
}
func printInfoAndLog(msg string, infoEv ...*zerolog.Event) {
fmt.Println(msg)
if len(infoEv) > 0 {
infoEv[0].Msg(msg)
} else {
gcutil.LogInfo().Str("source", "commandLine").Msg(msg)
}
}
func fatalAndLog(msg string, err error, fatalEv *zerolog.Event) {
fmt.Fprintln(os.Stderr, msg, err)
fatalEv.Err(err).Caller(1).Msg(msg)
}
func parseCommandLine() {
var newstaff string
var delstaff string
// var rebuild string
var password string
var rank int
var err error
if len(os.Args) < 2 {
return
}
cmd := os.Args[1]
var fatalEv *zerolog.Event
var systemCritical *config.SystemCriticalConfig
if cmd == "newstaff" || cmd == "delstaff" || cmd == "rebuild" {
if err = config.InitConfig(); err != nil {
fmt.Fprintln(os.Stderr, "Error initializing config:", err)
os.Exit(1)
}
systemCritical = config.GetSystemCriticalConfig()
if err = gcutil.InitLogs(systemCritical.LogDir, &gcutil.LogOptions{FileOnly: true}); err != nil {
fmt.Fprintln(os.Stderr, "Error initializing logs:", err)
os.Exit(1)
}
fatalEv = gcutil.LogFatal()
defer fatalEv.Discard()
initDB(fatalEv)
}
switch cmd {
case "version":
fmt.Println(config.GochanVersion)
return
case "help", "-h", "--help":
fmt.Println("Usage: gochan [command] [options]")
fmt.Println("Commands:")
fmt.Println(" version Show the version of gochan")
fmt.Println(" help Show this help message")
fmt.Println(" newstaff Create a new staff account")
fmt.Println(" delstaff Delete a staff account")
fmt.Println(" rebuild Rebuild the specified components")
fmt.Println("Run 'gochan [command] --help' for more information on a command.")
case "newstaff":
flagSet := flag.NewFlagSet("newstaff", flag.ExitOnError)
flagSet.StringVar(&newstaff, "username", "", "Username for the new staff account")
flagSet.StringVar(&password, "password", "", "Password for the new staff account")
flagSet.IntVar(&rank, "rank", 0, "Rank for the new staff account")
flagSet.Parse(os.Args[2:])
if newstaff == "" || rank <= 0 {
fmt.Fprintln(os.Stderr, "Error: -username and -rank are required")
flagSet.Usage()
os.Exit(1)
}
if password == "" {
fmt.Print("Enter password for new staff account: ")
password, err = getPassword()
if err != nil {
fmt.Fprintln(os.Stderr, "Error getting password:", err)
os.Exit(1)
}
if password == "" {
fmt.Fprintln(os.Stderr, "Error: Password cannot be empty")
os.Exit(1)
}
fmt.Print("Confirm password: ")
confirm, err := getPassword()
if err != nil {
fmt.Fprintln(os.Stderr, "Error getting password confirmation:", err)
os.Exit(1)
}
if password != confirm {
fmt.Fprintln(os.Stderr, "Error: Passwords do not match")
os.Exit(1)
}
}
staff, err := gcsql.NewStaff(newstaff, password, rank)
if err != nil {
fatalAndLog("Error creating new staff account:", err, fatalEv.Str("source", "commandLine").Str("username", newstaff))
}
printInfoAndLog("New staff account created successfully")
gcutil.LogInfo().
Str("source", "commandLine").
Str("username", newstaff).
Msg("New staff account created")
fmt.Printf("New staff account %q created with rank %s\n", newstaff, staff.RankTitle())
case "delstaff":
var force bool
flagSet := flag.NewFlagSet("delstaff", flag.ExitOnError)
flagSet.StringVar(&delstaff, "username", "", "Username of the staff account to delete")
flagSet.BoolVar(&force, "force", false, "Force deletion without confirmation")
flagSet.Parse(os.Args[2:])
if delstaff == "" {
fmt.Fprintln(os.Stderr, "Error: -username is required")
flagSet.Usage()
os.Exit(1)
}
if !force {
fmt.Printf("Are you sure you want to delete the staff account %q? [y/N]: ", delstaff)
var answer string
fmt.Scanln(&answer)
answer = strings.ToLower(answer)
if answer != "y" && answer != "yes" {
fmt.Println("Not deleting.")
return
}
}
if err = gcsql.DeactivateStaff(delstaff); err != nil {
fatalAndLog("Error deleting staff account:", err, fatalEv.Str("source", "commandLine").Str("username", delstaff))
}
printInfoAndLog("Staff account deleted successfully", gcutil.LogInfo().Str("source", "commandLine").Str("username", delstaff))
case "rebuild":
flagSet := flag.NewFlagSet("rebuild", flag.ExitOnError)
var rebuildAll bool
var rebuildBoards bool
var rebuildFront bool
var rebuildJS bool
flagSet.BoolVar(&rebuildBoards, "boards", false, "Rebuild boards and threads")
flagSet.BoolVar(&rebuildFront, "front", false, "Rebuild front page")
flagSet.BoolVar(&rebuildJS, "js", false, "Rebuild consts.js")
flagSet.BoolVar(&rebuildAll, "all", false, "Rebuild all components (overrides other flags)")
flagSet.Parse(os.Args[2:])
var rebuildFlag int
if rebuildAll {
rebuildFlag = buildAll
}
if rebuildBoards {
rebuildFlag |= buildBoards
}
if rebuildFront {
rebuildFlag |= buildFront
}
if rebuildJS {
rebuildFlag |= buildJS
}
if rebuildFlag == 0 {
fmt.Fprintln(os.Stderr, "Error: At least one rebuild option is required")
flagSet.Usage()
os.Exit(1)
}
startupRebuild(rebuildFlag, fatalEv)
default:
fmt.Fprintln(os.Stderr, "Unknown command:", cmd)
fmt.Println("Run 'gochan help' for a list of commands.")
os.Exit(1)
}
}

View file

@ -1,11 +1,8 @@
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"github.com/gochan-org/gochan/pkg/building"
@ -33,6 +30,10 @@ func cleanup() {
}
func main() {
if len(os.Args) > 1 {
parseCommandLine()
return
}
gcutil.LogInfo().Str("version", config.GochanVersion).Msg("Starting gochan")
fatalEv := gcutil.LogFatal()
defer func() {
@ -48,7 +49,11 @@ func main() {
uid, gid := config.GetUser()
systemCritical := config.GetSystemCriticalConfig()
if err = gcutil.InitLogs(systemCritical.LogDir, systemCritical.LogLevel(), uid, gid); err != nil {
if err = gcutil.InitLogs(systemCritical.LogDir, &gcutil.LogOptions{
LogLevel: systemCritical.LogLevel(),
UID: uid,
GID: gid,
}); err != nil {
fatalEv.Err(err).Caller().
Str("logDir", systemCritical.LogDir).
Int("uid", uid).
@ -70,25 +75,8 @@ func main() {
events.TriggerEvent("startup")
if err = gcsql.ConnectToDB(&systemCritical.SQLConfig); err != nil {
fatalEv.Err(err).Msg("Failed to connect to the database")
}
events.TriggerEvent("db-connected")
gcutil.LogInfo().
Str("DBtype", systemCritical.DBtype).
Str("DBhost", systemCritical.DBhost).
Msg("Connected to database")
initDB(fatalEv)
if err = gcsql.CheckAndInitializeDatabase(systemCritical.DBtype); err != nil {
cleanup()
gcutil.LogFatal().Err(err).Msg("Failed to initialize the database")
}
events.TriggerEvent("db-initialized")
if err = gcsql.ResetViews(); err != nil {
fatalEv.Err(err).Caller().Msg("Failed resetting SQL views")
}
parseCommandLine(fatalEv)
serverutil.InitMinifier()
siteCfg := config.GetSiteConfig()
if err = geoip.SetupGeoIP(siteCfg.GeoIPType, siteCfg.GeoIPOptions); err != nil {
@ -122,71 +110,24 @@ func main() {
<-sc
}
func parseCommandLine(fatalEv *zerolog.Event) {
var newstaff string
var delstaff string
var rebuild string
var rank int
var err error
flag.StringVar(&newstaff, "newstaff", "", "<newusername>:<newpassword>")
flag.StringVar(&delstaff, "delstaff", "", "<username>")
flag.StringVar(&rebuild, "rebuild", "", "accepted values are boards,front,js, or all")
flag.IntVar(&rank, "rank", 0, "New staff member rank, to be used with -newstaff or -delstaff")
flag.Parse()
func initDB(fatalEv *zerolog.Event) {
systemCritical := config.GetSystemCriticalConfig()
if err := gcsql.ConnectToDB(&systemCritical.SQLConfig); err != nil {
fatalEv.Err(err).Msg("Failed to connect to the database")
}
events.TriggerEvent("db-connected")
gcutil.LogInfo().
Str("DBtype", systemCritical.DBtype).
Str("DBhost", systemCritical.DBhost).
Msg("Connected to database")
rebuildFlag := buildNone
switch rebuild {
case "boards":
rebuildFlag = buildBoards
case "front":
rebuildFlag = buildFront
case "js":
rebuildFlag = buildJS
case "all":
rebuildFlag = buildAll
if err := gcsql.CheckAndInitializeDatabase(systemCritical.DBtype); err != nil {
cleanup()
gcutil.LogFatal().Err(err).Msg("Failed to initialize the database")
}
if rebuildFlag > 0 {
startupRebuild(rebuildFlag, fatalEv)
}
if newstaff != "" {
arr := strings.Split(newstaff, ":")
if len(arr) < 2 || delstaff != "" {
flag.Usage()
os.Exit(1)
}
if _, err = gcsql.NewStaff(arr[0], arr[1], rank); err != nil {
fatalEv.Err(err).Caller().
Str("source", "commandLine").
Str("username", arr[0]).
Msg("Failed creating new staff account")
}
gcutil.LogInfo().
Str("source", "commandLine").
Str("username", arr[0]).
Msg("New staff account created")
os.Exit(0)
}
if delstaff != "" {
if newstaff != "" {
flag.Usage()
os.Exit(1)
}
fmt.Printf("Are you sure you want to delete the staff account %q? [y/N]: ", delstaff)
var answer string
fmt.Scanln(&answer)
answer = strings.ToLower(answer)
if answer == "y" || answer == "yes" {
if err = gcsql.DeactivateStaff(delstaff); err != nil {
fatalEv.Err(err).Caller().
Str("source", "commandLine").
Str("username", delstaff).
Msg("Unable to delete staff account")
}
gcutil.LogInfo().Str("newStaff", delstaff).Msg("Staff account deleted")
} else {
fmt.Println("Not deleting.")
}
events.TriggerEvent("db-initialized")
if err := gcsql.ResetViews(); err != nil {
fatalEv.Err(err).Caller().Msg("Failed resetting SQL views")
}
events.TriggerEvent("db-views-reset")
}

View file

@ -1,12 +1,9 @@
package main
import (
"os"
"github.com/gochan-org/gochan/pkg/building"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gctemplates"
"github.com/gochan-org/gochan/pkg/gcutil"
"github.com/gochan-org/gochan/pkg/server/serverutil"
"github.com/rs/zerolog"
)
@ -23,44 +20,37 @@ func startupRebuild(buildFlag int, fatalEv *zerolog.Event) {
var err error
serverutil.InitMinifier()
if err = gctemplates.InitTemplates(); err != nil {
fatalEv.Err(err).Caller().
Str("building", "initialization").
Msg("Unable to initialize templates")
fatalAndLog("Unable to initialize templates:", err, fatalEv.Str("building", "initialization"))
}
if buildFlag&buildBoards > 0 {
gcsql.ResetBoardSectionArrays()
if err = building.BuildBoardListJSON(); err != nil {
fatalEv.Err(err).Caller().
Str("building", "sections").
Msg("Unable to build section array")
if err = gcsql.ResetBoardSectionArrays(); err != nil {
fatalAndLog("Unable to reset board section arrays:", err, fatalEv.Str("building", "reset"))
}
if err = building.BuildBoards(true); err != nil {
fatalEv.Err(err).Caller().
Str("building", "boards").
Msg("Unable to build boards")
if err = building.BuildBoardListJSON(); err != nil {
fatalAndLog("Unable to build board list JSON:", err, fatalEv.Str("building", "boardListJSON"))
}
gcutil.LogInfo().Msg("Boards built successfully")
printInfoAndLog("Board list JSON built successfully")
if err = building.BuildBoards(true); err != nil {
fatalAndLog("Unable to build boards:", err, fatalEv.Str("building", "boards"))
}
printInfoAndLog("Boards built successfully")
}
if buildFlag&buildJS > 0 {
if err = building.BuildJS(); err != nil {
fatalEv.Err(err).Caller().
Str("building", "js").
Msg("Unable to build consts.js")
fatalAndLog("Unable to build consts.js:", err, fatalEv.Str("building", "js"))
}
gcutil.LogInfo().Msg("consts.js built successfully")
printInfoAndLog("consts.js built successfully")
}
if buildFlag&buildFront > 0 {
if err = building.BuildFrontPage(); err != nil {
fatalEv.Err(err).Caller().
Str("building", "front").
Msg("Unable to build front page")
fatalAndLog("Unable to build front page:", err, fatalEv.Str("building", "front"))
}
gcutil.LogInfo().Msg("Front page built successfully")
printInfoAndLog("Front page built successfully")
}
gcutil.LogInfo().Msg("Finished building without errors, exiting.")
os.Exit(0)
printInfoAndLog("Finished building without errors, exiting.")
}

3
go.mod
View file

@ -29,6 +29,7 @@ require (
golang.org/x/crypto v0.37.0
golang.org/x/image v0.26.0
golang.org/x/net v0.39.0
golang.org/x/term v0.32.0
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf
layeh.com/gopher-luar v1.0.11
)
@ -50,7 +51,7 @@ require (
github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/test v1.0.11 // indirect
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.24.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

6
go.sum
View file

@ -259,8 +259,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -270,6 +270,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View file

@ -7,6 +7,6 @@
<h1>404: File not found</h1>
<img src="/error/lol 404.gif" alt="lol 404">
<p>The requested file could not be found on this server.</p>
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.1.0
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.2.0
</body>
</html>

View file

@ -7,6 +7,6 @@
<h1>Error 500: Internal Server error</h1>
<img src="/error/server500.gif" alt="server burning">
<p>The server encountered an error while trying to serve the page, and we apologize for the inconvenience. The <a href="https://en.wikipedia.org/wiki/Idiot">system administrator</a> will try to fix things as soon they get around to it, whenever that is. Hopefully soon.</p>
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.1.0
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.2.0
</body>
</html>

View file

@ -7,6 +7,6 @@
<h1>Error 502: Bad gateway</h1>
<img src="/error/server500.gif" alt="server burning">
<p>The server encountered an error while trying to serve the page, and we apologize for the inconvenience. The <a href="https://en.wikipedia.org/wiki/Idiot">system administrator</a> will try to fix things as soon they get around to it, whenever that is. Hopefully soon.</p>
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.1.0
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.2.0
</body>
</html>

View file

@ -30,7 +30,7 @@ const (
DefaultSQLMaxConns = 10
DefaultSQLConnMaxLifetimeMin = 3
GochanVersion = "4.1.0"
GochanVersion = "4.2.0"
)
var (

View file

@ -100,7 +100,7 @@ func init() {
})).With().Timestamp().Logger()
}
func initLog(logPath string, level zerolog.Level) (err error) {
func initLog(logPath string, level zerolog.Level, noConsole bool) (err error) {
if logFile != nil {
// log already initialized
if err = logFile.Close(); err != nil {
@ -114,10 +114,14 @@ func initLog(logPath string, level zerolog.Level) (err error) {
return err
}
writer := zerolog.MultiLevelWriter(logFile, zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
w.NoColor = !RunningInTerminal()
}))
var writer zerolog.LevelWriter
if noConsole {
writer = zerolog.MultiLevelWriter(logFile)
} else {
writer = zerolog.MultiLevelWriter(logFile, zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
w.NoColor = !RunningInTerminal()
}))
}
logger = zerolog.New(writer).With().Timestamp().Logger().Level(level)
return nil
@ -138,18 +142,41 @@ func initAccessLog(logPath string) (err error) {
return nil
}
func InitLogs(logDir string, level zerolog.Level, uid int, gid int) (err error) {
if err = initLog(path.Join(logDir, "gochan.log"), level); err != nil {
type LogOptions struct {
// LogLevel is the zerolog level to use for the log file.
LogLevel zerolog.Level
// UID is the user ID to set for the log file. If not set or 0, the current
// user ID will be used.
UID int
// GID is the group ID to set for the log file. If not set or 0, the current
// group ID will be used.
GID int
// FileOnly is true if the log file should be used only, and not the console
FileOnly bool
}
func InitLogs(logDir string, options *LogOptions) (err error) {
if options == nil {
options = &LogOptions{}
}
if err = initLog(path.Join(logDir, "gochan.log"), options.LogLevel, options.FileOnly); err != nil {
return err
}
if err = logFile.Chown(uid, gid); err != nil {
return err
if options.UID > 0 && options.GID > 0 {
if err = logFile.Chown(options.UID, options.GID); err != nil {
return err
}
}
if err = initAccessLog(path.Join(logDir, "gochan_access.log")); err != nil {
return err
}
return accessFile.Chown(uid, gid)
if options.UID > 0 && options.GID > 0 {
return accessFile.Chown(options.UID, options.GID)
}
return nil
}
func Logger() *zerolog.Logger {