diff --git a/.gitignore b/.gitignore index e578f95..ec61d27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env +.logs/ tmp/ projectreshoot static/css/output.css diff --git a/Makefile b/Makefile index 860fbd0..f2f1fd1 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ dev: test: go mod tidy && \ - go run . --port 3232 --test + go run . --port 3232 --test --loglevel trace clean: go clean diff --git a/db/users.go b/db/users.go index 27cc7a1..3cd038e 100644 --- a/db/users.go +++ b/db/users.go @@ -2,7 +2,6 @@ package db import ( "database/sql" - "fmt" "github.com/pkg/errors" "golang.org/x/crypto/bcrypt" @@ -27,7 +26,13 @@ func (user *User) SetPassword(conn *sql.DB, password string) error { if err != nil { return errors.Wrap(err, "conn.Exec") } - fmt.Println(result) + ra, err := result.RowsAffected() + if err != nil { + return errors.Wrap(err, "result.RowsAffected") + } + if ra != 1 { + return errors.New("Password was not updated") + } return nil } diff --git a/go.mod b/go.mod index 726d608..7e6b658 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/joho/godotenv v1.5.1 github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.33.0 github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d golang.org/x/crypto v0.33.0 ) @@ -14,5 +15,8 @@ require ( require ( github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/coder/websocket v1.8.12 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + golang.org/x/sys v0.30.0 // indirect ) diff --git a/go.sum b/go.sum index 7359dbf..aa1501a 100644 --- a/go.sum +++ b/go.sum @@ -4,17 +4,34 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU= github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/logging/logger.go b/logging/logger.go new file mode 100644 index 0000000..405b2f6 --- /dev/null +++ b/logging/logger.go @@ -0,0 +1,80 @@ +package logging + +import ( + "io" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/rs/zerolog/pkgerrors" +) + +// Takes a log level as string and converts it to a zerolog.Level interface. +// If the string is not a valid input it will return zerolog.InfoLevel +func GetLogLevel(level string) zerolog.Level { + levels := map[string]zerolog.Level{ + "trace": zerolog.TraceLevel, + "debug": zerolog.DebugLevel, + "info": zerolog.InfoLevel, + "warn": zerolog.WarnLevel, + "error": zerolog.ErrorLevel, + "fatal": zerolog.FatalLevel, + "panic": zerolog.PanicLevel, + } + logLevel, valid := levels[level] + if !valid { + return zerolog.InfoLevel + } + return logLevel +} + +// Returns a pointer to a new log file with the specified path. +// Remember to call file.Close() when finished writing to the log file +func GetLogFile(path string) (*os.File, error) { + logPath := filepath.Join(path, "server.log") + file, err := os.OpenFile( + logPath, + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0663, + ) + if err != nil { + return nil, errors.Wrap(err, "os.OpenFile") + } + return file, nil +} + +// Get a pointer to a new zerolog.Logger with the specified level and output +// Can provide a file, writer or both. Must provide at least one of the two +func GetLogger( + logLevel zerolog.Level, + w io.Writer, + logFile *os.File, + logDir string, +) (*zerolog.Logger, error) { + if w == nil && logFile == nil { + return nil, errors.New("No Writer provided for log output.") + } + + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack + + var consoleWriter zerolog.ConsoleWriter + if w != nil { + consoleWriter = zerolog.ConsoleWriter{Out: w} + } + + var output io.Writer + if logFile != nil { + if w != nil { + output = zerolog.MultiLevelWriter(logFile, consoleWriter) + } else { + output = logFile + } + } else { + output = consoleWriter + } + logger := zerolog.New(output).With().Timestamp().Caller().Logger().Level(logLevel) + + return &logger, nil +} diff --git a/main.go b/main.go index f8c20d8..11a93e8 100644 --- a/main.go +++ b/main.go @@ -15,13 +15,14 @@ import ( "time" "projectreshoot/db" + "projectreshoot/logging" "projectreshoot/server" "github.com/pkg/errors" ) // Initializes and runs the server -func run(ctx context.Context, w io.Writer, args []string) error { +func run(ctx context.Context, w io.Writer, args map[string]string) error { ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) defer cancel() @@ -30,10 +31,35 @@ func run(ctx context.Context, w io.Writer, args []string) error { return errors.Wrap(err, "server.GetConfig") } + var logfile *os.File = nil + if config.LogOutput == "both" || config.LogOutput == "file" { + logfile, err = logging.GetLogFile(config.LogDir) + if err != nil { + return errors.Wrap(err, "logging.GetLogFile") + } + defer logfile.Close() + } + + var consoleWriter io.Writer + if config.LogOutput == "both" || config.LogOutput == "console" { + consoleWriter = w + } + + logger, err := logging.GetLogger( + config.LogLevel, + consoleWriter, + logfile, + config.LogDir, + ) + if err != nil { + return errors.Wrap(err, "logging.GetLogger") + } + conn, err := db.ConnectToDatabase(&config.TursoDBName, &config.TursoToken) if err != nil { return errors.Wrap(err, "db.ConnectToDatabase") } + defer conn.Close() srv := server.NewServer(config, conn) httpServer := &http.Server{ @@ -41,9 +67,9 @@ func run(ctx context.Context, w io.Writer, args []string) error { Handler: srv, } - // TEST: runs function for testing in dev if --test flag true - if args[1] == "true" { - test(config, conn, httpServer) + // Runs function for testing in dev if --test flag true + if args["test"] == "true" { + test(config, logger, conn, httpServer) return nil } @@ -77,10 +103,24 @@ var static embed.FS // Start of runtime. Parse commandline arguments & flags, Initializes context // and starts the server func main() { - port := flag.String("port", "", "Override port") - test := flag.Bool("test", false, "Run test function") + // Parse commandline args + host := flag.String("host", "", "Override host to listen on") + port := flag.String("port", "", "Override port to listen on") + test := flag.Bool("test", false, "Run test function instead of main program") + loglevel := flag.String("loglevel", "", "Set log level") + logoutput := flag.String("logoutput", "", "Set log destination (file, console or both)") flag.Parse() - args := []string{*port, strconv.FormatBool(*test)} + + // Map the args for easy access + args := map[string]string{ + "host": *host, + "port": *port, + "test": strconv.FormatBool(*test), + "loglevel": *loglevel, + "logoutput": *logoutput, + } + + // Start the server ctx := context.Background() if err := run(ctx, os.Stdout, args); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) diff --git a/server/config.go b/server/config.go index e4c46b7..8c52c62 100644 --- a/server/config.go +++ b/server/config.go @@ -4,38 +4,78 @@ import ( "errors" "fmt" "os" + "projectreshoot/logging" "github.com/joho/godotenv" + "github.com/rs/zerolog" ) type Config struct { - Host string // Host to listen on - Port string // Port to listen on - TrustedHost string // Domain/Hostname to accept as trusted - TursoDBName string // DB Name for Turso DB/Branch - TursoToken string // Bearer token for Turso DB/Branch - SecretKey string // Secret key for signing tokens - AccessTokenExpiry int64 // Access token expiry in minutes - RefreshTokenExpiry int64 // Refresh token expiry in minutes - TokenFreshTime int64 // Time for tokens to stay fresh in minutes + Host string // Host to listen on + Port string // Port to listen on + TrustedHost string // Domain/Hostname to accept as trusted + TursoDBName string // DB Name for Turso DB/Branch + TursoToken string // Bearer token for Turso DB/Branch + SecretKey string // Secret key for signing tokens + AccessTokenExpiry int64 // Access token expiry in minutes + RefreshTokenExpiry int64 // Refresh token expiry in minutes + TokenFreshTime int64 // Time for tokens to stay fresh in minutes + LogLevel zerolog.Level // Log level for global logging. Defaults to info + LogOutput string // "file", "console", or "both". Defaults to console + LogDir string // Path to create log files } // Load the application configuration and get a pointer to the Config object -func GetConfig(args []string) (*Config, error) { +func GetConfig(args map[string]string) (*Config, error) { err := godotenv.Load(".env") if err != nil { fmt.Println(".env file not found.") } - var port string + var ( + host string + port string + logLevel zerolog.Level + logOutput string + valid bool + ) - if args[0] != "" { - port = args[0] + if args["host"] != "" { + host = args["host"] + } else { + host = GetEnvDefault("HOST", "127.0.0.1") + } + if args["port"] != "" { + port = args["port"] } else { port = GetEnvDefault("PORT", "3333") } + if args["loglevel"] != "" { + logLevel = logging.GetLogLevel(args["loglevel"]) + } else { + logLevel = logging.GetLogLevel(GetEnvDefault("LOG_LEVEL", "info")) + } + if args["logoutput"] != "" { + opts := map[string]string{ + "both": "both", + "file": "file", + "console": "console", + } + logOutput, valid = opts[args["logoutput"]] + if !valid { + logOutput = "console" + fmt.Println( + "Log output type was not parsed correctly. Defaulting to console only", + ) + } + } else { + logOutput = GetEnvDefault("LOG_OUTPUT", "console") + } + if logOutput != "both" && logOutput != "console" && logOutput != "file" { + logOutput = "console" + } config := &Config{ - Host: GetEnvDefault("HOST", "127.0.0.1"), + Host: host, Port: port, TrustedHost: os.Getenv("TRUSTED_HOST"), TursoDBName: os.Getenv("TURSO_DB_NAME"), @@ -44,6 +84,9 @@ func GetConfig(args []string) (*Config, error) { AccessTokenExpiry: GetEnvInt64("ACCESS_TOKEN_EXPIRY", 5), RefreshTokenExpiry: GetEnvInt64("REFRESH_TOKEN_EXPIRY", 1440), // defaults to 1 day TokenFreshTime: GetEnvInt64("TOKEN_FRESH_TIME", 5), + LogLevel: logLevel, + LogOutput: logOutput, + LogDir: GetEnvDefault("LOG_DIR", ""), } if config.TrustedHost == "" { diff --git a/tester.go b/tester.go index f3d7e2c..90d2f52 100644 --- a/tester.go +++ b/tester.go @@ -5,6 +5,8 @@ import ( "net/http" "projectreshoot/server" + + "github.com/rs/zerolog" ) // This function will only be called if the --test commandline flag is set. @@ -13,5 +15,10 @@ import ( // conflicts on the default 3333. Useful for testing things out during dev. // If you add code here, remember to run: // `git update-index --assume-unchanged tester.go` to avoid tracking changes -func test(config *server.Config, conn *sql.DB, srv *http.Server) { +func test( + config *server.Config, + logger *zerolog.Logger, + conn *sql.DB, + srv *http.Server, +) { }