Compare commits
4 Commits
8f6b4b0026
...
28b7ba34f0
| Author | SHA1 | Date | |
|---|---|---|---|
| 28b7ba34f0 | |||
| 4a21ba3821 | |||
| 1bcdf0e813 | |||
| 6dd80ee7b6 |
52
cmd/projectreshoot/auth.go
Normal file
52
cmd/projectreshoot/auth.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"projectreshoot/internal/config"
|
||||||
|
"projectreshoot/internal/handler"
|
||||||
|
"projectreshoot/internal/models"
|
||||||
|
"projectreshoot/pkg/contexts"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"git.haelnorr.com/h/golib/hwsauth"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupAuth(
|
||||||
|
config *config.Config,
|
||||||
|
logger *hlog.Logger,
|
||||||
|
conn *sql.DB,
|
||||||
|
server *hws.Server,
|
||||||
|
ignoredPaths []string,
|
||||||
|
) (*hwsauth.Authenticator[*models.User], error) {
|
||||||
|
auth, err := hwsauth.NewAuthenticator(
|
||||||
|
models.GetUserFromID,
|
||||||
|
server,
|
||||||
|
conn,
|
||||||
|
logger,
|
||||||
|
handler.NewErrorPage,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "hwsauth.NewAuthenticator")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.SSL = config.SSL
|
||||||
|
auth.AccessTokenExpiry = config.AccessTokenExpiry
|
||||||
|
auth.RefreshTokenExpiry = config.RefreshTokenExpiry
|
||||||
|
auth.TokenFreshTime = config.TokenFreshTime
|
||||||
|
auth.TrustedHost = config.TrustedHost
|
||||||
|
auth.SecretKey = config.SecretKey
|
||||||
|
auth.LandingPage = "/profile"
|
||||||
|
|
||||||
|
auth.IgnorePaths(ignoredPaths...)
|
||||||
|
|
||||||
|
err = auth.Initialise()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "auth.Initialise")
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts.CurrentUser = auth.CurrentModel
|
||||||
|
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ func setupFlags() map[string]string {
|
|||||||
host := flag.String("host", "", "Override host to listen on")
|
host := flag.String("host", "", "Override host to listen on")
|
||||||
port := flag.String("port", "", "Override port to listen on")
|
port := flag.String("port", "", "Override port to listen on")
|
||||||
test := flag.Bool("test", false, "Run server in test mode")
|
test := flag.Bool("test", false, "Run server in test mode")
|
||||||
tester := flag.Bool("tester", false, "Run tester function instead of main program")
|
|
||||||
dbver := flag.Bool("dbver", false, "Get the version of the database required")
|
dbver := flag.Bool("dbver", false, "Get the version of the database required")
|
||||||
loglevel := flag.String("loglevel", "", "Set log level")
|
loglevel := flag.String("loglevel", "", "Set log level")
|
||||||
logoutput := flag.String("logoutput", "", "Set log destination (file, console or both)")
|
logoutput := flag.String("logoutput", "", "Set log destination (file, console or both)")
|
||||||
@@ -21,7 +20,6 @@ func setupFlags() map[string]string {
|
|||||||
"host": *host,
|
"host": *host,
|
||||||
"port": *port,
|
"port": *port,
|
||||||
"test": strconv.FormatBool(*test),
|
"test": strconv.FormatBool(*test),
|
||||||
"tester": strconv.FormatBool(*tester),
|
|
||||||
"dbver": strconv.FormatBool(*dbver),
|
"dbver": strconv.FormatBool(*dbver),
|
||||||
"loglevel": *loglevel,
|
"loglevel": *loglevel,
|
||||||
"logoutput": *logoutput,
|
"logoutput": *logoutput,
|
||||||
|
|||||||
69
cmd/projectreshoot/httpserver.go
Normal file
69
cmd/projectreshoot/httpserver.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"projectreshoot/internal/config"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
|
"git.haelnorr.com/h/golib/jwt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupHttpServer(
|
||||||
|
staticFS *fs.FS,
|
||||||
|
config *config.Config,
|
||||||
|
logger *hlog.Logger,
|
||||||
|
conn *sql.DB,
|
||||||
|
tokenGen *jwt.TokenGenerator,
|
||||||
|
) (server *hws.Server, err error) {
|
||||||
|
if staticFS == nil {
|
||||||
|
return nil, errors.New("No filesystem provided")
|
||||||
|
}
|
||||||
|
fs := http.FS(*staticFS)
|
||||||
|
httpServer, err := hws.NewServer(
|
||||||
|
config.Host,
|
||||||
|
config.Port,
|
||||||
|
config.ReadHeaderTimeout,
|
||||||
|
config.WriteTimeout,
|
||||||
|
config.IdleTimeout,
|
||||||
|
config.GZIP,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "hws.NewServer")
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoredPaths := []string{
|
||||||
|
"/static/css/output.css",
|
||||||
|
"/static/favicon.ico",
|
||||||
|
}
|
||||||
|
|
||||||
|
auth, err := setupAuth(config, logger, conn, httpServer, ignoredPaths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "setupAuth")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = httpServer.AddLogger(logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "httpServer.AddLogger")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = httpServer.LoggerIgnorePaths(ignoredPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "httpServer.LoggerIgnorePaths")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = addRoutes(httpServer, &fs, config, logger, conn, tokenGen, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "addRoutes")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = addMiddleware(httpServer, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "httpServer.AddMiddleware")
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpServer, nil
|
||||||
|
}
|
||||||
46
cmd/projectreshoot/logger.go
Normal file
46
cmd/projectreshoot/logger.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Take in the desired logOutput and a console writer to use
|
||||||
|
func setupLogger(logLevel hlog.Level, logOutput string, w *io.Writer, logDirectory string) (*hlog.Logger, error) {
|
||||||
|
// Setup the logfile
|
||||||
|
var logfile *os.File = nil
|
||||||
|
if logOutput == "both" || logOutput == "file" {
|
||||||
|
logfile, err := hlog.NewLogFile(logDirectory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "hlog")
|
||||||
|
}
|
||||||
|
defer logfile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the console writer
|
||||||
|
var consoleWriter io.Writer
|
||||||
|
if logOutput == "both" || logOutput == "console" {
|
||||||
|
if w != nil {
|
||||||
|
consoleWriter = *w
|
||||||
|
} else {
|
||||||
|
if logOutput == "console" {
|
||||||
|
return nil, errors.New("Console logging specified as sole method but no writer provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the logger
|
||||||
|
logger, err := hlog.NewLogger(
|
||||||
|
logLevel,
|
||||||
|
consoleWriter,
|
||||||
|
logfile,
|
||||||
|
logDirectory,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "hlog")
|
||||||
|
}
|
||||||
|
return logger, nil
|
||||||
|
}
|
||||||
@@ -4,12 +4,22 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"projectreshoot/internal/config"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
args := setupFlags()
|
args := setupFlags()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := run(ctx, os.Stdout, args); err != nil {
|
|
||||||
|
config, err := config.GetConfig(args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", errors.Wrap(err, "Failed to load config"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := run(ctx, os.Stdout, args, config); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
23
cmd/projectreshoot/middleware.go
Normal file
23
cmd/projectreshoot/middleware.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"git.haelnorr.com/h/golib/hwsauth"
|
||||||
|
"projectreshoot/internal/models"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addMiddleware(
|
||||||
|
server *hws.Server,
|
||||||
|
auth *hwsauth.Authenticator[*models.User],
|
||||||
|
) error {
|
||||||
|
|
||||||
|
err := server.AddMiddleware(
|
||||||
|
auth.Authenticate(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "server.AddMiddleware")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
127
cmd/projectreshoot/routes.go
Normal file
127
cmd/projectreshoot/routes.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"git.haelnorr.com/h/golib/hwsauth"
|
||||||
|
"net/http"
|
||||||
|
"projectreshoot/internal/config"
|
||||||
|
"projectreshoot/internal/handler"
|
||||||
|
"projectreshoot/internal/models"
|
||||||
|
"projectreshoot/internal/view/page"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
|
"git.haelnorr.com/h/golib/jwt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addRoutes(
|
||||||
|
server *hws.Server,
|
||||||
|
staticFS *http.FileSystem,
|
||||||
|
config *config.Config,
|
||||||
|
logger *hlog.Logger,
|
||||||
|
conn *sql.DB,
|
||||||
|
tokenGen *jwt.TokenGenerator,
|
||||||
|
auth *hwsauth.Authenticator[*models.User],
|
||||||
|
) error {
|
||||||
|
// Create the routes
|
||||||
|
routes := []hws.Route{
|
||||||
|
{
|
||||||
|
Path: "/static/",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Handler: http.StripPrefix("/static/", handler.StaticFS(staticFS, logger)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Handler: handler.Root(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/about",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Handler: handler.HandlePage(page.About()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/login",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Handler: auth.LogoutReq(handler.LoginPage(config.TrustedHost)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/login",
|
||||||
|
Method: hws.MethodPOST,
|
||||||
|
Handler: auth.LogoutReq(handler.LoginRequest(server, auth, conn)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/register",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Handler: auth.LogoutReq(handler.RegisterPage(config.TrustedHost)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/register",
|
||||||
|
Method: hws.MethodPOST,
|
||||||
|
Handler: auth.LogoutReq(handler.RegisterRequest(config, logger, conn, tokenGen)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/logout",
|
||||||
|
Method: hws.MethodPOST,
|
||||||
|
Handler: handler.Logout(server, auth, conn),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/reauthenticate",
|
||||||
|
Method: hws.MethodPOST,
|
||||||
|
Handler: auth.LoginReq(handler.Reauthenticate(server, auth, conn)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/profile",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Handler: auth.LoginReq(handler.ProfilePage()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/account",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Handler: auth.LoginReq(handler.AccountPage()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/account-select-page",
|
||||||
|
Method: hws.MethodPOST,
|
||||||
|
Handler: auth.LoginReq(handler.AccountSubpage()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/change-username",
|
||||||
|
Method: hws.MethodPOST,
|
||||||
|
Handler: auth.LoginReq(auth.FreshReq(handler.ChangeUsername(server, auth, conn))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/change-password",
|
||||||
|
Method: hws.MethodPOST,
|
||||||
|
Handler: auth.LoginReq(auth.FreshReq(handler.ChangePassword(server, auth, conn))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/change-bio",
|
||||||
|
Method: hws.MethodPOST,
|
||||||
|
Handler: auth.LoginReq(handler.ChangeBio(server, auth, conn)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/movies",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Handler: handler.MoviesPage(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/search-movies",
|
||||||
|
Method: hws.MethodPOST,
|
||||||
|
Handler: handler.SearchMovies(config, logger),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/movie/{movie_id}",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Handler: handler.Movie(config, logger),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the routes with the server
|
||||||
|
err := server.AddRoutes(routes...)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "server.AddRoutes")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,64 +4,32 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"projectreshoot/internal/httpserver"
|
"projectreshoot/internal/config"
|
||||||
"projectreshoot/pkg/config"
|
|
||||||
"projectreshoot/pkg/embedfs"
|
"projectreshoot/pkg/embedfs"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
"git.haelnorr.com/h/golib/jwt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var maint uint32 // atomic: 1 if in maintenance mode
|
|
||||||
|
|
||||||
// Initializes and runs the server
|
// Initializes and runs the server
|
||||||
func run(ctx context.Context, w io.Writer, args map[string]string) error {
|
func run(ctx context.Context, w io.Writer, args map[string]string, config *config.Config) error {
|
||||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
config, err := config.GetConfig(args)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "server.GetConfig")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the version of the database required
|
// Return the version of the database required
|
||||||
if args["dbver"] == "true" {
|
if args["dbver"] == "true" {
|
||||||
fmt.Fprintf(w, "Database version: %s\n", config.DBName)
|
fmt.Fprintf(w, "Database version: %s\n", config.DBName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup the logfile
|
logger, err := setupLogger(config.LogLevel, config.LogOutput, &w, config.LogDir)
|
||||||
var logfile *os.File = nil
|
|
||||||
if config.LogOutput == "both" || config.LogOutput == "file" {
|
|
||||||
logfile, err = hlog.NewLogFile(config.LogDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "logging.GetLogFile")
|
return errors.Wrap(err, "setupLogger")
|
||||||
}
|
|
||||||
defer logfile.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup the console writer
|
|
||||||
var consoleWriter io.Writer
|
|
||||||
if config.LogOutput == "both" || config.LogOutput == "console" {
|
|
||||||
consoleWriter = w
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup the logger
|
|
||||||
logger, err := hlog.NewLogger(
|
|
||||||
config.LogLevel,
|
|
||||||
consoleWriter,
|
|
||||||
logfile,
|
|
||||||
config.LogDir,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "logging.GetLogger")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup the database connection
|
// Setup the database connection
|
||||||
@@ -92,19 +60,17 @@ func run(ctx context.Context, w io.Writer, args map[string]string) error {
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.Debug().Msg("Setting up HTTP server")
|
logger.Debug().Msg("Setting up HTTP server")
|
||||||
httpServer := httpserver.NewServer(config, logger, conn, tokenGen, &staticFS, &maint)
|
httpServer, err := setupHttpServer(&staticFS, config, logger, conn, tokenGen)
|
||||||
|
if err != nil {
|
||||||
// Setups a channel to listen for os.Signal
|
return errors.Wrap(err, "setupHttpServer")
|
||||||
handleMaintSignals(httpServer, logger)
|
}
|
||||||
|
|
||||||
// Runs the http server
|
// Runs the http server
|
||||||
logger.Debug().Msg("Starting up the HTTP server")
|
logger.Debug().Msg("Starting up the HTTP server")
|
||||||
go func() {
|
err = httpServer.Start()
|
||||||
logger.Info().Str("address", httpServer.Addr).Msg("Listening for requests")
|
if err != nil {
|
||||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
return errors.Wrap(err, "httpServer.Start")
|
||||||
logger.Error().Err(err).Msg("Error listening and serving")
|
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
// Handles graceful shutdown
|
// Handles graceful shutdown
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -113,9 +79,7 @@ func run(ctx context.Context, w io.Writer, args map[string]string) error {
|
|||||||
shutdownCtx := context.Background()
|
shutdownCtx := context.Background()
|
||||||
shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
httpServer.Shutdown(shutdownCtx)
|
||||||
logger.Error().Err(err).Msg("Error shutting down server")
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
logger.Info().Msg("Shutting down")
|
logger.Info().Msg("Shutting down")
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"sync/atomic"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle SIGUSR1 and SIGUSR2 syscalls to toggle maintenance mode
|
|
||||||
func handleMaintSignals(
|
|
||||||
srv *http.Server,
|
|
||||||
logger *hlog.Logger,
|
|
||||||
) {
|
|
||||||
logger.Debug().Msg("Starting signal listener")
|
|
||||||
ch := make(chan os.Signal, 1)
|
|
||||||
srv.RegisterOnShutdown(func() {
|
|
||||||
logger.Debug().Msg("Shutting down signal listener")
|
|
||||||
close(ch)
|
|
||||||
})
|
|
||||||
go func() {
|
|
||||||
for sig := range ch {
|
|
||||||
switch sig {
|
|
||||||
case syscall.SIGUSR1:
|
|
||||||
if atomic.LoadUint32(&maint) != 1 {
|
|
||||||
atomic.StoreUint32(&maint, 1)
|
|
||||||
logger.Info().Msg("Signal received: Starting maintenance")
|
|
||||||
}
|
|
||||||
case syscall.SIGUSR2:
|
|
||||||
if atomic.LoadUint32(&maint) != 0 {
|
|
||||||
logger.Info().Msg("Signal received: Maintenance over")
|
|
||||||
atomic.StoreUint32(&maint, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
signal.Notify(ch, syscall.SIGUSR1, syscall.SIGUSR2)
|
|
||||||
}
|
|
||||||
8
go.mod
8
go.mod
@@ -3,8 +3,12 @@ module projectreshoot
|
|||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.haelnorr.com/h/golib/cookies v0.9.0
|
||||||
|
git.haelnorr.com/h/golib/env v0.9.0
|
||||||
git.haelnorr.com/h/golib/hlog v0.9.0
|
git.haelnorr.com/h/golib/hlog v0.9.0
|
||||||
git.haelnorr.com/h/golib/jwt v0.9.0
|
git.haelnorr.com/h/golib/hws v0.1.0
|
||||||
|
git.haelnorr.com/h/golib/hwsauth v0.2.0
|
||||||
|
git.haelnorr.com/h/golib/jwt v0.9.2
|
||||||
git.haelnorr.com/h/golib/tmdb v0.8.0
|
git.haelnorr.com/h/golib/tmdb v0.8.0
|
||||||
github.com/a-h/templ v0.3.977
|
github.com/a-h/templ v0.3.977
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
@@ -15,8 +19,6 @@ require (
|
|||||||
modernc.org/sqlite v1.35.0
|
modernc.org/sqlite v1.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace git.haelnorr.com/h/golib/jwt v0.9.0 => /home/haelnorr/projects/golib/jwt
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -1,5 +1,15 @@
|
|||||||
|
git.haelnorr.com/h/golib/cookies v0.9.0 h1:Vf+eX1prHkKuGrQon1BHY87yaPc1H+HJFRXDOV/AuWs=
|
||||||
|
git.haelnorr.com/h/golib/cookies v0.9.0/go.mod h1:y1385YExI9gLwckCVDCYVcsFXr6N7T3brJjnJD2QIuo=
|
||||||
|
git.haelnorr.com/h/golib/env v0.9.0 h1:Ahqr3PbHy7HdWEHUhylzIZy6Gg8mST5UdgKlU2RAhls=
|
||||||
|
git.haelnorr.com/h/golib/env v0.9.0/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
||||||
git.haelnorr.com/h/golib/hlog v0.9.0 h1:ib8n2MdmiRK2TF067p220kXmhDe9aAnlcsgpuv+QpvE=
|
git.haelnorr.com/h/golib/hlog v0.9.0 h1:ib8n2MdmiRK2TF067p220kXmhDe9aAnlcsgpuv+QpvE=
|
||||||
git.haelnorr.com/h/golib/hlog v0.9.0/go.mod h1:oOlzb8UVHUYP1k7dN5PSJXVskAB2z8EYgRN85jAi0Zk=
|
git.haelnorr.com/h/golib/hlog v0.9.0/go.mod h1:oOlzb8UVHUYP1k7dN5PSJXVskAB2z8EYgRN85jAi0Zk=
|
||||||
|
git.haelnorr.com/h/golib/hws v0.1.0 h1:+0eNq1uGWrGfbS5AgHeGoGDjVfCWuaVu+1wBxgPqyOY=
|
||||||
|
git.haelnorr.com/h/golib/hws v0.1.0/go.mod h1:b2pbkMaebzmck9TxqGBGzTJPEcB5TWcEHwFknLE7dqM=
|
||||||
|
git.haelnorr.com/h/golib/hwsauth v0.2.0 h1:rLfTtxo0lBUMuWzEdoS1Y4i8/UiCzDZ5DS+6WC/C974=
|
||||||
|
git.haelnorr.com/h/golib/hwsauth v0.2.0/go.mod h1:d1oXUstDHqKwCXzcEMdHGC8yoT2S2gwpJkrEo8daCMs=
|
||||||
|
git.haelnorr.com/h/golib/jwt v0.9.2 h1:l1Ow7DPGACAU54CnMP/NlZjdc4nRD1wr3xZ8a7taRvU=
|
||||||
|
git.haelnorr.com/h/golib/jwt v0.9.2/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4=
|
||||||
git.haelnorr.com/h/golib/tmdb v0.8.0 h1:OQ6M2TB8FHm8fJD7/ebfWm63Duzfp0kmFX9genEig34=
|
git.haelnorr.com/h/golib/tmdb v0.8.0 h1:OQ6M2TB8FHm8fJD7/ebfWm63Duzfp0kmFX9genEig34=
|
||||||
git.haelnorr.com/h/golib/tmdb v0.8.0/go.mod h1:mGKYa3o3z0IsQ5EO3MPmnL2Bwl2sSMsUHXVgaIGR7Z0=
|
git.haelnorr.com/h/golib/tmdb v0.8.0/go.mod h1:mGKYa3o3z0IsQ5EO3MPmnL2Bwl2sSMsUHXVgaIGR7Z0=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/env"
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
"git.haelnorr.com/h/golib/tmdb"
|
"git.haelnorr.com/h/golib/tmdb"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
@@ -47,17 +48,17 @@ func GetConfig(args map[string]string) (*Config, error) {
|
|||||||
if args["host"] != "" {
|
if args["host"] != "" {
|
||||||
host = args["host"]
|
host = args["host"]
|
||||||
} else {
|
} else {
|
||||||
host = GetEnvDefault("HOST", "127.0.0.1")
|
host = env.String("HOST", "127.0.0.1")
|
||||||
}
|
}
|
||||||
if args["port"] != "" {
|
if args["port"] != "" {
|
||||||
port = args["port"]
|
port = args["port"]
|
||||||
} else {
|
} else {
|
||||||
port = GetEnvDefault("PORT", "3010")
|
port = env.String("PORT", "3010")
|
||||||
}
|
}
|
||||||
if args["loglevel"] != "" {
|
if args["loglevel"] != "" {
|
||||||
logLevel = hlog.LogLevel(args["loglevel"])
|
logLevel = hlog.LogLevel(args["loglevel"])
|
||||||
} else {
|
} else {
|
||||||
logLevel = hlog.LogLevel(GetEnvDefault("LOG_LEVEL", "info"))
|
logLevel = hlog.LogLevel(env.String("LOG_LEVEL", "info"))
|
||||||
}
|
}
|
||||||
if args["logoutput"] != "" {
|
if args["logoutput"] != "" {
|
||||||
opts := map[string]string{
|
opts := map[string]string{
|
||||||
@@ -73,7 +74,7 @@ func GetConfig(args map[string]string) (*Config, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logOutput = GetEnvDefault("LOG_OUTPUT", "console")
|
logOutput = env.String("LOG_OUTPUT", "console")
|
||||||
}
|
}
|
||||||
if logOutput != "both" && logOutput != "console" && logOutput != "file" {
|
if logOutput != "both" && logOutput != "console" && logOutput != "file" {
|
||||||
logOutput = "console"
|
logOutput = "console"
|
||||||
@@ -86,22 +87,22 @@ func GetConfig(args map[string]string) (*Config, error) {
|
|||||||
config := &Config{
|
config := &Config{
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
TrustedHost: GetEnvDefault("TRUSTED_HOST", "127.0.0.1"),
|
TrustedHost: env.String("TRUSTED_HOST", "127.0.0.1"),
|
||||||
SSL: GetEnvBool("SSL_MODE", false),
|
SSL: env.Bool("SSL_MODE", false),
|
||||||
GZIP: GetEnvBool("GZIP", false),
|
GZIP: env.Bool("GZIP", false),
|
||||||
ReadHeaderTimeout: GetEnvDur("READ_HEADER_TIMEOUT", 2),
|
ReadHeaderTimeout: env.Duration("READ_HEADER_TIMEOUT", 2),
|
||||||
WriteTimeout: GetEnvDur("WRITE_TIMEOUT", 10),
|
WriteTimeout: env.Duration("WRITE_TIMEOUT", 10),
|
||||||
IdleTimeout: GetEnvDur("IDLE_TIMEOUT", 120),
|
IdleTimeout: env.Duration("IDLE_TIMEOUT", 120),
|
||||||
DBName: "00001",
|
DBName: "00001",
|
||||||
DBLockTimeout: GetEnvDur("DB_LOCK_TIMEOUT", 60),
|
DBLockTimeout: env.Duration("DB_LOCK_TIMEOUT", 60),
|
||||||
SecretKey: os.Getenv("SECRET_KEY"),
|
SecretKey: env.String("SECRET_KEY", ""),
|
||||||
AccessTokenExpiry: GetEnvInt64("ACCESS_TOKEN_EXPIRY", 5),
|
AccessTokenExpiry: env.Int64("ACCESS_TOKEN_EXPIRY", 5),
|
||||||
RefreshTokenExpiry: GetEnvInt64("REFRESH_TOKEN_EXPIRY", 1440), // defaults to 1 day
|
RefreshTokenExpiry: env.Int64("REFRESH_TOKEN_EXPIRY", 1440), // defaults to 1 day
|
||||||
TokenFreshTime: GetEnvInt64("TOKEN_FRESH_TIME", 5),
|
TokenFreshTime: env.Int64("TOKEN_FRESH_TIME", 5),
|
||||||
LogLevel: logLevel,
|
LogLevel: logLevel,
|
||||||
LogOutput: logOutput,
|
LogOutput: logOutput,
|
||||||
LogDir: GetEnvDefault("LOG_DIR", ""),
|
LogDir: env.String("LOG_DIR", ""),
|
||||||
TMDBToken: os.Getenv("TMDB_API_TOKEN"),
|
TMDBToken: env.String("TMDB_API_TOKEN", ""),
|
||||||
TMDBConfig: tmdbcfg,
|
TMDBConfig: tmdbcfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6,13 +6,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"git.haelnorr.com/h/golib/hwsauth"
|
||||||
"projectreshoot/internal/models"
|
"projectreshoot/internal/models"
|
||||||
"projectreshoot/internal/view/component/account"
|
"projectreshoot/internal/view/component/account"
|
||||||
"projectreshoot/internal/view/page"
|
"projectreshoot/internal/view/page"
|
||||||
"projectreshoot/pkg/contexts"
|
|
||||||
"projectreshoot/pkg/cookies"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
"git.haelnorr.com/h/golib/cookies"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@@ -45,7 +45,8 @@ func AccountSubpage() http.Handler {
|
|||||||
|
|
||||||
// Handles a request to change the users username
|
// Handles a request to change the users username
|
||||||
func ChangeUsername(
|
func ChangeUsername(
|
||||||
logger *hlog.Logger,
|
server *hws.Server,
|
||||||
|
auth *hwsauth.Authenticator[*models.User],
|
||||||
conn *sql.DB,
|
conn *sql.DB,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
@@ -56,8 +57,7 @@ func ChangeUsername(
|
|||||||
// Start the transaction
|
// Start the transaction
|
||||||
tx, err := conn.BeginTx(ctx, nil)
|
tx, err := conn.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("Error updating username")
|
server.ThrowWarn(w, hws.NewError(http.StatusServiceUnavailable, "Error updating username", err))
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
@@ -65,8 +65,7 @@ func ChangeUsername(
|
|||||||
unique, err := models.CheckUsernameUnique(tx, newUsername)
|
unique, err := models.CheckUsernameUnique(tx, newUsername)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
logger.Error().Err(err).Msg("Error updating username")
|
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Error updating username", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !unique {
|
if !unique {
|
||||||
@@ -75,12 +74,11 @@ func ChangeUsername(
|
|||||||
Render(r.Context(), w)
|
Render(r.Context(), w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := contexts.GetUser(r.Context())
|
user := auth.CurrentModel(r.Context())
|
||||||
err = user.ChangeUsername(tx, newUsername)
|
err = user.ChangeUsername(tx, newUsername)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
logger.Error().Err(err).Msg("Error updating username")
|
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Error updating username", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
@@ -91,7 +89,8 @@ func ChangeUsername(
|
|||||||
|
|
||||||
// Handles a request to change the users bio
|
// Handles a request to change the users bio
|
||||||
func ChangeBio(
|
func ChangeBio(
|
||||||
logger *hlog.Logger,
|
server *hws.Server,
|
||||||
|
auth *hwsauth.Authenticator[*models.User],
|
||||||
conn *sql.DB,
|
conn *sql.DB,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
@@ -102,8 +101,7 @@ func ChangeBio(
|
|||||||
// Start the transaction
|
// Start the transaction
|
||||||
tx, err := conn.BeginTx(ctx, nil)
|
tx, err := conn.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("Error updating bio")
|
server.ThrowWarn(w, hws.NewError(http.StatusServiceUnavailable, "Error updating bio", err))
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
@@ -115,12 +113,11 @@ func ChangeBio(
|
|||||||
Render(r.Context(), w)
|
Render(r.Context(), w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := contexts.GetUser(r.Context())
|
user := auth.CurrentModel(r.Context())
|
||||||
err = user.ChangeBio(tx, newBio)
|
err = user.ChangeBio(tx, newBio)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
logger.Error().Err(err).Msg("Error updating bio")
|
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Error updating bio", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
@@ -145,7 +142,8 @@ func validateChangePassword(
|
|||||||
|
|
||||||
// Handles a request to change the users password
|
// Handles a request to change the users password
|
||||||
func ChangePassword(
|
func ChangePassword(
|
||||||
logger *hlog.Logger,
|
server *hws.Server,
|
||||||
|
auth *hwsauth.Authenticator[*models.User],
|
||||||
conn *sql.DB,
|
conn *sql.DB,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
@@ -156,8 +154,7 @@ func ChangePassword(
|
|||||||
// Start the transaction
|
// Start the transaction
|
||||||
tx, err := conn.BeginTx(ctx, nil)
|
tx, err := conn.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("Error updating password")
|
server.ThrowWarn(w, hws.NewError(http.StatusServiceUnavailable, "Error updating password", err))
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newPass, err := validateChangePassword(r)
|
newPass, err := validateChangePassword(r)
|
||||||
@@ -166,12 +163,11 @@ func ChangePassword(
|
|||||||
account.ChangePassword(err.Error()).Render(r.Context(), w)
|
account.ChangePassword(err.Error()).Render(r.Context(), w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := contexts.GetUser(r.Context())
|
user := auth.CurrentModel(r.Context())
|
||||||
err = user.SetPassword(tx, newPass)
|
err = user.SetPassword(tx, newPass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
logger.Error().Err(err).Msg("Error updating password")
|
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Error updating password", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
|
|||||||
@@ -22,3 +22,21 @@ func ErrorPage(
|
|||||||
page.Error(errorCode, http.StatusText(errorCode), message[errorCode]).
|
page.Error(errorCode, http.StatusText(errorCode), message[errorCode]).
|
||||||
Render(r.Context(), w)
|
Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewErrorPage(
|
||||||
|
errorCode int,
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
) error {
|
||||||
|
message := map[int]string{
|
||||||
|
401: "You need to login to view this page.",
|
||||||
|
403: "You do not have permission to view this page.",
|
||||||
|
404: "The page or resource you have requested does not exist.",
|
||||||
|
500: `An error occured on the server. Please try again, and if this
|
||||||
|
continues to happen contact an administrator.`,
|
||||||
|
503: "The server is currently down for maintenance and should be back soon. =)",
|
||||||
|
}
|
||||||
|
w.WriteHeader(errorCode)
|
||||||
|
return page.Error(errorCode, http.StatusText(errorCode), message[errorCode]).
|
||||||
|
Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"git.haelnorr.com/h/golib/hwsauth"
|
||||||
"projectreshoot/internal/models"
|
"projectreshoot/internal/models"
|
||||||
"projectreshoot/internal/view/component/form"
|
"projectreshoot/internal/view/component/form"
|
||||||
"projectreshoot/internal/view/page"
|
"projectreshoot/internal/view/page"
|
||||||
"projectreshoot/pkg/config"
|
|
||||||
"projectreshoot/pkg/cookies"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
"git.haelnorr.com/h/golib/cookies"
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,6 +32,9 @@ func validateLogin(
|
|||||||
|
|
||||||
err = user.CheckPassword(tx, formPassword)
|
err = user.CheckPassword(tx, formPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "Username or password incorrect") {
|
||||||
|
return nil, errors.Wrap(err, "user.CheckPassword")
|
||||||
|
}
|
||||||
return nil, errors.New("Username or password incorrect")
|
return nil, errors.New("Username or password incorrect")
|
||||||
}
|
}
|
||||||
return user, nil
|
return user, nil
|
||||||
@@ -51,10 +54,9 @@ func checkRememberMe(r *http.Request) bool {
|
|||||||
// and on fail will return the login form again, passing the error to the
|
// and on fail will return the login form again, passing the error to the
|
||||||
// template for user feedback
|
// template for user feedback
|
||||||
func LoginRequest(
|
func LoginRequest(
|
||||||
config *config.Config,
|
server *hws.Server,
|
||||||
logger *hlog.Logger,
|
auth *hwsauth.Authenticator[*models.User],
|
||||||
conn *sql.DB,
|
conn *sql.DB,
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -64,8 +66,7 @@ func LoginRequest(
|
|||||||
// Start the transaction
|
// Start the transaction
|
||||||
tx, err := conn.BeginTx(ctx, nil)
|
tx, err := conn.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("Failed to set token cookies")
|
server.ThrowWarn(w, hws.NewError(http.StatusServiceUnavailable, "Login failed", err))
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
@@ -73,8 +74,7 @@ func LoginRequest(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
if err.Error() != "Username or password incorrect" {
|
if err.Error() != "Username or password incorrect" {
|
||||||
logger.Warn().Caller().Err(err).Msg("Login request failed")
|
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Login failed", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
} else {
|
} else {
|
||||||
form.LoginForm(err.Error()).Render(r.Context(), w)
|
form.LoginForm(err.Error()).Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
@@ -82,11 +82,10 @@ func LoginRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
rememberMe := checkRememberMe(r)
|
rememberMe := checkRememberMe(r)
|
||||||
err = cookies.SetTokenCookies(w, r, config, tokenGen, user, true, rememberMe)
|
err = auth.Login(w, r, user, rememberMe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
server.ThrowWarn(w, hws.NewError(http.StatusInternalServerError, "Login failed", err))
|
||||||
logger.Warn().Caller().Err(err).Msg("Failed to set token cookies")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,84 +3,18 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"git.haelnorr.com/h/golib/hwsauth"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"projectreshoot/internal/models"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"projectreshoot/pkg/cookies"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func revokeAccess(
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
tx *sql.Tx,
|
|
||||||
atStr string,
|
|
||||||
) error {
|
|
||||||
aT, err := tokenGen.ValidateAccess(tx, atStr)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "Token is expired") ||
|
|
||||||
strings.Contains(err.Error(), "Token has been revoked") {
|
|
||||||
return nil // Token is expired, dont need to revoke it
|
|
||||||
}
|
|
||||||
return errors.Wrap(err, "jwt.ParseAccessToken")
|
|
||||||
}
|
|
||||||
err = aT.Revoke(tx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "jwt.RevokeToken")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func revokeRefresh(
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
tx *sql.Tx,
|
|
||||||
rtStr string,
|
|
||||||
) error {
|
|
||||||
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "Token is expired") ||
|
|
||||||
strings.Contains(err.Error(), "Token has been revoked") {
|
|
||||||
return nil // Token is expired, dont need to revoke it
|
|
||||||
}
|
|
||||||
return errors.Wrap(err, "jwt.ParseRefreshToken")
|
|
||||||
}
|
|
||||||
err = rT.Revoke(tx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "jwt.RevokeToken")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve and revoke the user's tokens
|
|
||||||
func revokeTokens(
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
tx *sql.Tx,
|
|
||||||
r *http.Request,
|
|
||||||
) error {
|
|
||||||
// get the tokens from the cookies
|
|
||||||
atStr, rtStr := cookies.GetTokenStrings(r)
|
|
||||||
// revoke the refresh token first as the access token expires quicker
|
|
||||||
// only matters if there is an error revoking the tokens
|
|
||||||
err := revokeRefresh(tokenGen, tx, rtStr)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "revokeRefresh")
|
|
||||||
}
|
|
||||||
err = revokeAccess(tokenGen, tx, atStr)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "revokeAccess")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle a logout request
|
// Handle a logout request
|
||||||
func Logout(
|
func Logout(
|
||||||
|
server *hws.Server,
|
||||||
|
auth *hwsauth.Authenticator[*models.User],
|
||||||
conn *sql.DB,
|
conn *sql.DB,
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
logger *hlog.Logger,
|
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -89,21 +23,17 @@ func Logout(
|
|||||||
|
|
||||||
tx, err := conn.BeginTx(ctx, nil)
|
tx, err := conn.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Failed to start database transaction")
|
server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Logout failed", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
err = revokeTokens(tokenGen, tx, r)
|
err = auth.Logout(tx, w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Error occured on user logout")
|
server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Logout failed", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
cookies.DeleteCookie(w, "access", "/")
|
|
||||||
cookies.DeleteCookie(w, "refresh", "/")
|
|
||||||
w.Header().Set("HX-Redirect", "/login")
|
w.Header().Set("HX-Redirect", "/login")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"projectreshoot/internal/config"
|
||||||
"projectreshoot/internal/view/page"
|
"projectreshoot/internal/view/page"
|
||||||
"projectreshoot/pkg/config"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Movie(
|
func Movie(
|
||||||
logger *hlog.Logger,
|
|
||||||
config *config.Config,
|
config *config.Config,
|
||||||
|
logger *hlog.Logger,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"projectreshoot/internal/config"
|
||||||
"projectreshoot/internal/view/component/search"
|
"projectreshoot/internal/view/component/search"
|
||||||
"projectreshoot/internal/view/page"
|
"projectreshoot/internal/view/page"
|
||||||
"projectreshoot/pkg/config"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
"git.haelnorr.com/h/golib/tmdb"
|
"git.haelnorr.com/h/golib/tmdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SearchMovies(
|
func SearchMovies(
|
||||||
logger *hlog.Logger,
|
|
||||||
config *config.Config,
|
config *config.Config,
|
||||||
|
logger *hlog.Logger,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -6,91 +6,23 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"git.haelnorr.com/h/golib/hwsauth"
|
||||||
|
"projectreshoot/internal/models"
|
||||||
"projectreshoot/internal/view/component/form"
|
"projectreshoot/internal/view/component/form"
|
||||||
"projectreshoot/pkg/config"
|
|
||||||
"projectreshoot/pkg/contexts"
|
|
||||||
"projectreshoot/pkg/cookies"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get the tokens from the request
|
|
||||||
func getTokens(
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
tx *sql.Tx,
|
|
||||||
r *http.Request,
|
|
||||||
) (*jwt.AccessToken, *jwt.RefreshToken, error) {
|
|
||||||
// get the existing tokens from the cookies
|
|
||||||
atStr, rtStr := cookies.GetTokenStrings(r)
|
|
||||||
aT, err := tokenGen.ValidateAccess(tx, atStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errors.Wrap(err, "tokenGen.ValidateAccess")
|
|
||||||
}
|
|
||||||
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errors.Wrap(err, "tokenGen.ValidateRefresh")
|
|
||||||
}
|
|
||||||
return aT, rT, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Revoke the given token pair
|
|
||||||
func revokeTokenPair(
|
|
||||||
tx *sql.Tx,
|
|
||||||
aT *jwt.AccessToken,
|
|
||||||
rT *jwt.RefreshToken,
|
|
||||||
) error {
|
|
||||||
err := aT.Revoke(tx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "aT.Revoke")
|
|
||||||
}
|
|
||||||
err = rT.Revoke(tx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "rT.Revoke")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issue new tokens for the user, invalidating the old ones
|
|
||||||
func refreshTokens(
|
|
||||||
config *config.Config,
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
tx *sql.Tx,
|
|
||||||
w http.ResponseWriter,
|
|
||||||
r *http.Request,
|
|
||||||
) error {
|
|
||||||
aT, rT, err := getTokens(tokenGen, tx, r)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "getTokens")
|
|
||||||
}
|
|
||||||
rememberMe := map[string]bool{
|
|
||||||
"session": false,
|
|
||||||
"exp": true,
|
|
||||||
}[aT.TTL]
|
|
||||||
// issue new tokens for the user
|
|
||||||
user := contexts.GetUser(r.Context())
|
|
||||||
err = cookies.SetTokenCookies(w, r, config, tokenGen, user.User, true, rememberMe)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "cookies.SetTokenCookies")
|
|
||||||
}
|
|
||||||
err = revokeTokenPair(tx, aT, rT)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "revokeTokenPair")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the provided password
|
// Validate the provided password
|
||||||
func validatePassword(
|
func validatePassword(
|
||||||
|
auth *hwsauth.Authenticator[*models.User],
|
||||||
tx *sql.Tx,
|
tx *sql.Tx,
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
) error {
|
) error {
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
user := contexts.GetUser(r.Context())
|
user := auth.CurrentModel(r.Context())
|
||||||
err := user.CheckPassword(tx, password)
|
err := user.CheckPassword(tx, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "user.CheckPassword")
|
return errors.Wrap(err, "user.CheckPassword")
|
||||||
@@ -100,10 +32,9 @@ func validatePassword(
|
|||||||
|
|
||||||
// Handle request to reauthenticate (i.e. make token fresh again)
|
// Handle request to reauthenticate (i.e. make token fresh again)
|
||||||
func Reauthenticate(
|
func Reauthenticate(
|
||||||
logger *hlog.Logger,
|
server *hws.Server,
|
||||||
config *config.Config,
|
auth *hwsauth.Authenticator[*models.User],
|
||||||
conn *sql.DB,
|
conn *sql.DB,
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -113,21 +44,19 @@ func Reauthenticate(
|
|||||||
// Start the transaction
|
// Start the transaction
|
||||||
tx, err := conn.BeginTx(ctx, nil)
|
tx, err := conn.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Failed to start transaction")
|
server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Failed to start transaction", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
err = validatePassword(tx, r)
|
err = validatePassword(auth, tx, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(445)
|
w.WriteHeader(445)
|
||||||
form.ConfirmPassword("Incorrect password").Render(r.Context(), w)
|
form.ConfirmPassword("Incorrect password").Render(r.Context(), w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = refreshTokens(config, tokenGen, tx, w, r)
|
err = auth.RefreshAuthTokens(tx, w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Failed to refresh user tokens")
|
server.ThrowError(w, r, hws.NewError(http.StatusInternalServerError, "Failed to refresh user tokens", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"projectreshoot/internal/config"
|
||||||
"projectreshoot/internal/models"
|
"projectreshoot/internal/models"
|
||||||
"projectreshoot/internal/view/component/form"
|
"projectreshoot/internal/view/component/form"
|
||||||
"projectreshoot/internal/view/page"
|
"projectreshoot/internal/view/page"
|
||||||
"projectreshoot/pkg/config"
|
|
||||||
"projectreshoot/pkg/cookies"
|
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/cookies"
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
"git.haelnorr.com/h/golib/jwt"
|
||||||
|
|
||||||
@@ -48,9 +48,9 @@ func validateRegistration(
|
|||||||
|
|
||||||
func RegisterRequest(
|
func RegisterRequest(
|
||||||
config *config.Config,
|
config *config.Config,
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
logger *hlog.Logger,
|
logger *hlog.Logger,
|
||||||
conn *sql.DB,
|
conn *sql.DB,
|
||||||
|
tokenGen *jwt.TokenGenerator,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -80,7 +80,7 @@ func RegisterRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
rememberMe := checkRememberMe(r)
|
rememberMe := checkRememberMe(r)
|
||||||
err = cookies.SetTokenCookies(w, r, config, tokenGen, user, true, rememberMe)
|
err = jwt.SetTokenCookies(w, r, tokenGen, user.ID(), true, rememberMe, config.SSL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -1,52 +1,23 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Wrapper for default FileSystem
|
|
||||||
type justFilesFilesystem struct {
|
|
||||||
fs http.FileSystem
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrapper for default File
|
|
||||||
type neuteredReaddirFile struct {
|
|
||||||
http.File
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modifies the behavior of FileSystem.Open to return the neutered version of File
|
|
||||||
func (fs justFilesFilesystem) Open(name string) (http.File, error) {
|
|
||||||
f, err := fs.fs.Open(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the requested path is a directory
|
|
||||||
// and explicitly return an error to trigger a 404
|
|
||||||
fileInfo, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if fileInfo.IsDir() {
|
|
||||||
return nil, os.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
return neuteredReaddirFile{f}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overrides the Readdir method of File to always return nil
|
|
||||||
func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles requests for static files, without allowing access to the
|
// Handles requests for static files, without allowing access to the
|
||||||
// directory viewer and returning 404 if an exact file is not found
|
// directory viewer and returning 404 if an exact file is not found
|
||||||
func StaticFS(staticFS *http.FileSystem) http.Handler {
|
func StaticFS(staticFS *http.FileSystem, logger *hlog.Logger) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
nfs := justFilesFilesystem{*staticFS}
|
fs, err := hws.SafeFileServer(staticFS)
|
||||||
fs := http.FileServer(nfs)
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
logger.Error().Err(err).Msg("Failed to load file system")
|
||||||
|
return
|
||||||
|
}
|
||||||
fs.ServeHTTP(w, r)
|
fs.ServeHTTP(w, r)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
package httpserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"projectreshoot/internal/handler"
|
|
||||||
"projectreshoot/internal/middleware"
|
|
||||||
"projectreshoot/internal/view/page"
|
|
||||||
"projectreshoot/pkg/config"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add all the handled routes to the mux
|
|
||||||
func addRoutes(
|
|
||||||
mux *http.ServeMux,
|
|
||||||
logger *hlog.Logger,
|
|
||||||
config *config.Config,
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
conn *sql.DB,
|
|
||||||
staticFS *http.FileSystem,
|
|
||||||
) {
|
|
||||||
route := mux.Handle
|
|
||||||
loggedIn := middleware.LoginReq
|
|
||||||
loggedOut := middleware.LogoutReq
|
|
||||||
fresh := middleware.FreshReq
|
|
||||||
|
|
||||||
// Health check
|
|
||||||
mux.HandleFunc("GET /healthz", func(http.ResponseWriter, *http.Request) {})
|
|
||||||
|
|
||||||
// Static files
|
|
||||||
route("GET /static/", http.StripPrefix("/static/", handler.StaticFS(staticFS)))
|
|
||||||
|
|
||||||
// Index page and unhandled catchall (404)
|
|
||||||
route("GET /", handler.Root())
|
|
||||||
|
|
||||||
// Static content, unprotected pages
|
|
||||||
route("GET /about", handler.HandlePage(page.About()))
|
|
||||||
|
|
||||||
// Login page and handlers
|
|
||||||
route("GET /login", loggedOut(handler.LoginPage(config.TrustedHost)))
|
|
||||||
route("POST /login", loggedOut(handler.LoginRequest(config, logger, conn, tokenGen)))
|
|
||||||
|
|
||||||
// Register page and handlers
|
|
||||||
route("GET /register", loggedOut(handler.RegisterPage(config.TrustedHost)))
|
|
||||||
route("POST /register", loggedOut(handler.RegisterRequest(config, tokenGen, logger, conn)))
|
|
||||||
|
|
||||||
// Logout
|
|
||||||
route("POST /logout", handler.Logout(conn, tokenGen, logger))
|
|
||||||
|
|
||||||
// Reauthentication request
|
|
||||||
route("POST /reauthenticate", loggedIn(handler.Reauthenticate(logger, config, conn, tokenGen)))
|
|
||||||
|
|
||||||
// Profile page
|
|
||||||
route("GET /profile", loggedIn(handler.ProfilePage()))
|
|
||||||
|
|
||||||
// Account page
|
|
||||||
route("GET /account", loggedIn(handler.AccountPage()))
|
|
||||||
route("POST /account-select-page", loggedIn(handler.AccountSubpage()))
|
|
||||||
route("POST /change-username", loggedIn(fresh(handler.ChangeUsername(logger, conn))))
|
|
||||||
route("POST /change-bio", loggedIn(handler.ChangeBio(logger, conn)))
|
|
||||||
route("POST /change-password", loggedIn(fresh(handler.ChangePassword(logger, conn))))
|
|
||||||
|
|
||||||
// Movies Search
|
|
||||||
route("GET /movies", handler.MoviesPage())
|
|
||||||
route("POST /search-movies", handler.SearchMovies(logger, config))
|
|
||||||
|
|
||||||
// Movie page
|
|
||||||
route("GET /movie/{movie_id}", handler.Movie(logger, config))
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package httpserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"io/fs"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"projectreshoot/internal/middleware"
|
|
||||||
"projectreshoot/pkg/config"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewServer(
|
|
||||||
config *config.Config,
|
|
||||||
logger *hlog.Logger,
|
|
||||||
conn *sql.DB,
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
staticFS *fs.FS,
|
|
||||||
maint *uint32,
|
|
||||||
) *http.Server {
|
|
||||||
fs := http.FS(*staticFS)
|
|
||||||
srv := createServer(config, logger, conn, tokenGen, &fs, maint)
|
|
||||||
httpServer := &http.Server{
|
|
||||||
Addr: net.JoinHostPort(config.Host, config.Port),
|
|
||||||
Handler: srv,
|
|
||||||
ReadHeaderTimeout: config.ReadHeaderTimeout * time.Second,
|
|
||||||
WriteTimeout: config.WriteTimeout * time.Second,
|
|
||||||
IdleTimeout: config.IdleTimeout * time.Second,
|
|
||||||
}
|
|
||||||
return httpServer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a new http.Handler with all the routes and middleware added
|
|
||||||
func createServer(
|
|
||||||
config *config.Config,
|
|
||||||
logger *hlog.Logger,
|
|
||||||
conn *sql.DB,
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
staticFS *http.FileSystem,
|
|
||||||
maint *uint32,
|
|
||||||
) http.Handler {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
addRoutes(
|
|
||||||
mux,
|
|
||||||
logger,
|
|
||||||
config,
|
|
||||||
tokenGen,
|
|
||||||
conn,
|
|
||||||
staticFS,
|
|
||||||
)
|
|
||||||
var handler http.Handler = mux
|
|
||||||
// Add middleware here, must be added in reverse order of execution
|
|
||||||
// i.e. First in list will get executed last during the request handling
|
|
||||||
handler = middleware.Logging(logger, handler)
|
|
||||||
handler = middleware.Authentication(logger, config, conn, tokenGen, handler, maint)
|
|
||||||
|
|
||||||
// Gzip
|
|
||||||
handler = middleware.Gzip(handler, config.GZIP)
|
|
||||||
|
|
||||||
// Start the timer for the request chain so logger can have accurate info
|
|
||||||
handler = middleware.StartTimer(handler)
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"projectreshoot/internal/handler"
|
|
||||||
"projectreshoot/internal/models"
|
|
||||||
"projectreshoot/pkg/config"
|
|
||||||
"projectreshoot/pkg/contexts"
|
|
||||||
"projectreshoot/pkg/cookies"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Attempt to use a valid refresh token to generate a new token pair
|
|
||||||
func refreshAuthTokens(
|
|
||||||
config *config.Config,
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
tx *sql.Tx,
|
|
||||||
w http.ResponseWriter,
|
|
||||||
req *http.Request,
|
|
||||||
ref *jwt.RefreshToken,
|
|
||||||
) (*models.User, error) {
|
|
||||||
user, err := models.GetUserFromID(tx, ref.SUB)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "models.GetUser")
|
|
||||||
}
|
|
||||||
|
|
||||||
rememberMe := map[string]bool{
|
|
||||||
"session": false,
|
|
||||||
"exp": true,
|
|
||||||
}[ref.TTL]
|
|
||||||
|
|
||||||
// Set fresh to true because new tokens coming from refresh request
|
|
||||||
err = cookies.SetTokenCookies(w, req, config, tokenGen, user, false, rememberMe)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cookies.SetTokenCookies")
|
|
||||||
}
|
|
||||||
// New tokens sent, revoke the used refresh token
|
|
||||||
err = ref.Revoke(tx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "ref.Revoke")
|
|
||||||
}
|
|
||||||
// Return the authorized user
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the cookies for token strings and attempt to authenticate them
|
|
||||||
func getAuthenticatedUser(
|
|
||||||
config *config.Config,
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
tx *sql.Tx,
|
|
||||||
w http.ResponseWriter,
|
|
||||||
r *http.Request,
|
|
||||||
) (*contexts.AuthenticatedUser, error) {
|
|
||||||
// Get token strings from cookies
|
|
||||||
atStr, rtStr := cookies.GetTokenStrings(r)
|
|
||||||
if atStr == "" && rtStr == "" {
|
|
||||||
return nil, errors.New("No token strings provided")
|
|
||||||
}
|
|
||||||
// Attempt to parse the access token
|
|
||||||
aT, err := tokenGen.ValidateAccess(tx, atStr)
|
|
||||||
if err != nil {
|
|
||||||
// Access token invalid, attempt to parse refresh token
|
|
||||||
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "tokenGen.ValidateRefresh")
|
|
||||||
}
|
|
||||||
// Refresh token valid, attempt to get a new token pair
|
|
||||||
user, err := refreshAuthTokens(config, tokenGen, tx, w, r, rT)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "refreshAuthTokens")
|
|
||||||
}
|
|
||||||
// New token pair sent, return the authorized user
|
|
||||||
authUser := contexts.AuthenticatedUser{
|
|
||||||
User: user,
|
|
||||||
Fresh: time.Now().Unix(),
|
|
||||||
}
|
|
||||||
return &authUser, nil
|
|
||||||
}
|
|
||||||
// Access token valid
|
|
||||||
user, err := models.GetUserFromID(tx, aT.SUB)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "models.GetUser")
|
|
||||||
}
|
|
||||||
authUser := contexts.AuthenticatedUser{
|
|
||||||
User: user,
|
|
||||||
Fresh: aT.Fresh,
|
|
||||||
}
|
|
||||||
return &authUser, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to authenticate the user and add their account details
|
|
||||||
// to the request context
|
|
||||||
func Authentication(
|
|
||||||
logger *hlog.Logger,
|
|
||||||
config *config.Config,
|
|
||||||
conn *sql.DB,
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
next http.Handler,
|
|
||||||
maint *uint32,
|
|
||||||
) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path == "/static/css/output.css" ||
|
|
||||||
r.URL.Path == "/static/favicon.ico" {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if atomic.LoadUint32(maint) == 1 {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the transaction
|
|
||||||
tx, err := conn.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
// Failed to start transaction, skip auth
|
|
||||||
logger.Warn().Err(err).
|
|
||||||
Msg("Skipping Auth - unable to start a transaction")
|
|
||||||
handler.ErrorPage(http.StatusServiceUnavailable, w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user, err := getAuthenticatedUser(config, tokenGen, tx, w, r)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
// User auth failed, delete the cookies to avoid repeat requests
|
|
||||||
cookies.DeleteCookie(w, "access", "/")
|
|
||||||
cookies.DeleteCookie(w, "refresh", "/")
|
|
||||||
logger.Debug().
|
|
||||||
Str("remote_addr", r.RemoteAddr).
|
|
||||||
Err(err).
|
|
||||||
Msg("Failed to authenticate user")
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tx.Commit()
|
|
||||||
uctx := contexts.SetUser(r.Context(), user)
|
|
||||||
newReq := r.WithContext(uctx)
|
|
||||||
next.ServeHTTP(w, newReq)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/gzip"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Gzip(next http.Handler, useGzip bool) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") ||
|
|
||||||
!useGzip {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Encoding", "gzip")
|
|
||||||
gz := gzip.NewWriter(w)
|
|
||||||
defer gz.Close()
|
|
||||||
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
|
||||||
next.ServeHTTP(gzw, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type gzipResponseWriter struct {
|
|
||||||
io.Writer
|
|
||||||
http.ResponseWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
|
||||||
return w.Writer.Write(b)
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"projectreshoot/internal/handler"
|
|
||||||
"projectreshoot/pkg/contexts"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wraps the http.ResponseWriter, adding a statusCode field
|
|
||||||
type wrappedWriter struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
statusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extends WriteHeader to the ResponseWriter to add the status code
|
|
||||||
func (w *wrappedWriter) WriteHeader(statusCode int) {
|
|
||||||
w.ResponseWriter.WriteHeader(statusCode)
|
|
||||||
w.statusCode = statusCode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Middleware to add logs to console with details of the request
|
|
||||||
func Logging(logger *hlog.Logger, next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path == "/static/css/output.css" ||
|
|
||||||
r.URL.Path == "/static/favicon.ico" {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
start, err := contexts.GetStartTime(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
handler.ErrorPage(http.StatusInternalServerError, w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
wrapped := &wrappedWriter{
|
|
||||||
ResponseWriter: w,
|
|
||||||
statusCode: http.StatusOK,
|
|
||||||
}
|
|
||||||
next.ServeHTTP(wrapped, r)
|
|
||||||
logger.Info().
|
|
||||||
Int("status", wrapped.statusCode).
|
|
||||||
Str("method", r.Method).
|
|
||||||
Str("resource", r.URL.Path).
|
|
||||||
Dur("time_elapsed", time.Since(start)).
|
|
||||||
Str("remote_addr", r.Header.Get("X-Forwarded-For")).
|
|
||||||
Msg("Served")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"projectreshoot/internal/handler"
|
|
||||||
"projectreshoot/pkg/contexts"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Checks if the user is set in the context and shows 401 page if not logged in
|
|
||||||
func LoginReq(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user := contexts.GetUser(r.Context())
|
|
||||||
if user == nil {
|
|
||||||
handler.ErrorPage(http.StatusUnauthorized, w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the user is set in the context and redirects them to profile if
|
|
||||||
// they are logged in
|
|
||||||
func LogoutReq(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user := contexts.GetUser(r.Context())
|
|
||||||
if user != nil {
|
|
||||||
http.Redirect(w, r, "/profile", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"projectreshoot/pkg/contexts"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FreshReq(
|
|
||||||
next http.Handler,
|
|
||||||
) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user := contexts.GetUser(r.Context())
|
|
||||||
isFresh := time.Now().Before(time.Unix(user.Fresh, 0))
|
|
||||||
if !isFresh {
|
|
||||||
w.WriteHeader(444)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"projectreshoot/pkg/contexts"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func StartTimer(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(
|
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
start := time.Now()
|
|
||||||
ctx := contexts.SetStart(r.Context(), start)
|
|
||||||
newReq := r.WithContext(ctx)
|
|
||||||
next.ServeHTTP(w, newReq)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -8,12 +8,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int // Integer ID (index primary key)
|
id int // Integer ID (index primary key)
|
||||||
Username string // Username (unique)
|
Username string // Username (unique)
|
||||||
Created_at int64 // Epoch timestamp when the user was added to the database
|
Created_at int64 // Epoch timestamp when the user was added to the database
|
||||||
Bio string // Short byline set by the user
|
Bio string // Short byline set by the user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u User) ID() int {
|
||||||
|
return u.id
|
||||||
|
}
|
||||||
|
|
||||||
// Uses bcrypt to set the users Password_hash from the given password
|
// Uses bcrypt to set the users Password_hash from the given password
|
||||||
func (user *User) SetPassword(
|
func (user *User) SetPassword(
|
||||||
tx *sql.Tx,
|
tx *sql.Tx,
|
||||||
@@ -25,7 +29,7 @@ func (user *User) SetPassword(
|
|||||||
}
|
}
|
||||||
newPassword := string(hashedPassword)
|
newPassword := string(hashedPassword)
|
||||||
query := `UPDATE users SET password_hash = ? WHERE id = ?`
|
query := `UPDATE users SET password_hash = ? WHERE id = ?`
|
||||||
_, err = tx.Exec(query, newPassword, user.ID)
|
_, err = tx.Exec(query, newPassword, user.id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "tx.Exec")
|
return errors.Wrap(err, "tx.Exec")
|
||||||
}
|
}
|
||||||
@@ -35,8 +39,8 @@ func (user *User) SetPassword(
|
|||||||
// Uses bcrypt to check if the given password matches the users Password_hash
|
// Uses bcrypt to check if the given password matches the users Password_hash
|
||||||
func (user *User) CheckPassword(tx *sql.Tx, password string) error {
|
func (user *User) CheckPassword(tx *sql.Tx, password string) error {
|
||||||
query := `SELECT password_hash FROM users WHERE id = ? LIMIT 1`
|
query := `SELECT password_hash FROM users WHERE id = ? LIMIT 1`
|
||||||
row := tx.QueryRow(query, user.ID)
|
row := tx.QueryRow(query, user.id)
|
||||||
hashedPassword := ""
|
var hashedPassword string
|
||||||
err := row.Scan(&hashedPassword)
|
err := row.Scan(&hashedPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "row.Scan")
|
return errors.Wrap(err, "row.Scan")
|
||||||
@@ -51,7 +55,7 @@ func (user *User) CheckPassword(tx *sql.Tx, password string) error {
|
|||||||
// Change the user's username
|
// Change the user's username
|
||||||
func (user *User) ChangeUsername(tx *sql.Tx, newUsername string) error {
|
func (user *User) ChangeUsername(tx *sql.Tx, newUsername string) error {
|
||||||
query := `UPDATE users SET username = ? WHERE id = ?`
|
query := `UPDATE users SET username = ? WHERE id = ?`
|
||||||
_, err := tx.Exec(query, newUsername, user.ID)
|
_, err := tx.Exec(query, newUsername, user.id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "tx.Exec")
|
return errors.Wrap(err, "tx.Exec")
|
||||||
}
|
}
|
||||||
@@ -61,7 +65,7 @@ func (user *User) ChangeUsername(tx *sql.Tx, newUsername string) error {
|
|||||||
// Change the user's bio
|
// Change the user's bio
|
||||||
func (user *User) ChangeBio(tx *sql.Tx, newBio string) error {
|
func (user *User) ChangeBio(tx *sql.Tx, newBio string) error {
|
||||||
query := `UPDATE users SET bio = ? WHERE id = ?`
|
query := `UPDATE users SET bio = ? WHERE id = ?`
|
||||||
_, err := tx.Exec(query, newBio, user.ID)
|
_, err := tx.Exec(query, newBio, user.id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "tx.Exec")
|
return errors.Wrap(err, "tx.Exec")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func scanUserRow(user *User, rows *sql.Rows) error {
|
|||||||
return errors.New("User not found")
|
return errors.New("User not found")
|
||||||
}
|
}
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&user.ID,
|
&user.id,
|
||||||
&user.Username,
|
&user.Username,
|
||||||
&user.Created_at,
|
&user.Created_at,
|
||||||
&user.Bio,
|
&user.Bio,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package account
|
|||||||
import "projectreshoot/pkg/contexts"
|
import "projectreshoot/pkg/contexts"
|
||||||
|
|
||||||
templ ChangeBio(err string, bio string) {
|
templ ChangeBio(err string, bio string) {
|
||||||
|
{{ user := contexts.CurrentUser(ctx) }}
|
||||||
{{
|
{{
|
||||||
user := contexts.GetUser(ctx)
|
|
||||||
if bio == "" {
|
if bio == "" {
|
||||||
bio = user.Bio
|
bio = user.Bio
|
||||||
}
|
}
|
||||||
@@ -21,21 +21,20 @@ templ ChangeBio(err string, bio string) {
|
|||||||
bio: newBio,
|
bio: newBio,
|
||||||
initialBio: oldBio,
|
initialBio: oldBio,
|
||||||
err: err,
|
err: err,
|
||||||
bioLenText: '',
|
bioLenText: "",
|
||||||
updateTextArea() {
|
updateTextArea() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.bio) {
|
if (this.$refs.bio) {
|
||||||
this.$refs.bio.style.height = 'auto';
|
this.$refs.bio.style.height = "auto";
|
||||||
this.$refs.bio.style.height = `
|
this.$refs.bio.style.height = `
|
||||||
${this.$refs.bio.scrollHeight + 20}px`;
|
${this.$refs.bio.scrollHeight + 20}px`;
|
||||||
};
|
}
|
||||||
this.bioLenText = `${this.bio.length}/128`;
|
this.bioLenText = `${this.bio.length}/128`;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetBio() {
|
resetBio() {
|
||||||
this.bio = this.initialBio;
|
this.bio = this.initialBio;
|
||||||
this.err = "",
|
((this.err = ""), this.updateTextArea());
|
||||||
this.updateTextArea();
|
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -46,7 +45,7 @@ templ ChangeBio(err string, bio string) {
|
|||||||
this.updateTextArea();
|
this.updateTextArea();
|
||||||
}, 20);
|
}, 20);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package account
|
|||||||
import "projectreshoot/pkg/contexts"
|
import "projectreshoot/pkg/contexts"
|
||||||
|
|
||||||
templ ChangeUsername(err string, username string) {
|
templ ChangeUsername(err string, username string) {
|
||||||
|
{{ user := contexts.CurrentUser(ctx) }}
|
||||||
{{
|
{{
|
||||||
user := contexts.GetUser(ctx)
|
|
||||||
if username == "" {
|
if username == "" {
|
||||||
username = user.Username
|
username = user.Username
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ templ AccountContainer(subpage string) {
|
|||||||
@resize.window="big = window.innerWidth >= 768"
|
@resize.window="big = window.innerWidth >= 768"
|
||||||
>
|
>
|
||||||
@SelectMenu(subpage)
|
@SelectMenu(subpage)
|
||||||
<div class="mt-5 w-full md:ml-[200px] ml-[40px] transition-all duration-300">
|
<div class="mt-5 w-full md:ml-[200px] ml-10 transition-all duration-300">
|
||||||
<div
|
<div
|
||||||
class="pl-5 text-2xl text-subtext1 border-b
|
class="pl-5 text-2xl text-subtext1 border-b
|
||||||
border-overlay0 w-[90%] mx-auto"
|
border-overlay0 w-[90%] mx-auto"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ templ Navbar() {
|
|||||||
<div x-data="{ open: false }">
|
<div x-data="{ open: false }">
|
||||||
<header class="bg-crust">
|
<header class="bg-crust">
|
||||||
<div
|
<div
|
||||||
class="mx-auto flex h-16 max-w-screen-xl items-center gap-8
|
class="mx-auto flex h-16 max-w-7xl items-center gap-8
|
||||||
px-4 sm:px-6 lg:px-8"
|
px-4 sm:px-6 lg:px-8"
|
||||||
>
|
>
|
||||||
<a class="block" href="/">
|
<a class="block" href="/">
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func getProfileItems() []ProfileItem {
|
|||||||
|
|
||||||
// Returns the right portion of the navbar
|
// Returns the right portion of the navbar
|
||||||
templ navRight() {
|
templ navRight() {
|
||||||
{{ user := contexts.GetUser(ctx) }}
|
{{ user := contexts.CurrentUser(ctx) }}
|
||||||
{{ items := getProfileItems() }}
|
{{ items := getProfileItems() }}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="sm:flex sm:gap-2">
|
<div class="sm:flex sm:gap-2">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import "projectreshoot/pkg/contexts"
|
|||||||
|
|
||||||
// Returns the mobile version of the navbar thats only visible when activated
|
// Returns the mobile version of the navbar thats only visible when activated
|
||||||
templ sideNav(navItems []NavItem) {
|
templ sideNav(navItems []NavItem) {
|
||||||
{{ user := contexts.GetUser(ctx) }}
|
{{ user := contexts.CurrentUser(ctx) }}
|
||||||
<div
|
<div
|
||||||
x-show="open"
|
x-show="open"
|
||||||
x-transition
|
x-transition
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ templ Global(title string) {
|
|||||||
(function () {
|
(function () {
|
||||||
let theme = localStorage.getItem("theme") || "system";
|
let theme = localStorage.getItem("theme") || "system";
|
||||||
if (theme === "system") {
|
if (theme === "system") {
|
||||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
}
|
}
|
||||||
if (theme === "dark") {
|
if (theme === "dark") {
|
||||||
document.documentElement.classList.add("dark");
|
document.documentElement.classList.add("dark");
|
||||||
@@ -65,18 +67,18 @@ templ Global(title string) {
|
|||||||
const errorCode = event.detail.errorInfo.error;
|
const errorCode = event.detail.errorInfo.error;
|
||||||
|
|
||||||
// internal server error
|
// internal server error
|
||||||
if (errorCode.includes('Code 500')) {
|
if (errorCode.includes("Code 500")) {
|
||||||
this.showError500 = true;
|
this.showError500 = true;
|
||||||
setTimeout(() => this.showError500 = false, 6000);
|
setTimeout(() => (this.showError500 = false), 6000);
|
||||||
}
|
}
|
||||||
// service not available error
|
// service not available error
|
||||||
if (errorCode.includes('Code 503')) {
|
if (errorCode.includes("Code 503")) {
|
||||||
this.showError503 = true;
|
this.showError503 = true;
|
||||||
setTimeout(() => this.showError503 = false, 6000);
|
setTimeout(() => (this.showError503 = false), 6000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// user is authorized but needs to refresh their login
|
// user is authorized but needs to refresh their login
|
||||||
if (errorCode.includes('Code 444')) {
|
if (errorCode.includes("Code 444")) {
|
||||||
this.showConfirmPasswordModal = true;
|
this.showConfirmPasswordModal = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import "projectreshoot/internal/view/layout"
|
|||||||
import "projectreshoot/pkg/contexts"
|
import "projectreshoot/pkg/contexts"
|
||||||
|
|
||||||
templ Profile() {
|
templ Profile() {
|
||||||
{{ user := contexts.GetUser(ctx) }}
|
{{ user := contexts.CurrentUser(ctx) }}
|
||||||
@layout.Global("Profile - " + user.Username) {
|
@layout.Global("Profile - " + user.Username) {
|
||||||
<div class="">
|
<div class="">
|
||||||
Hello, { user.Username }
|
Hello, { user.Username }
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get an environment variable, specifying a default value if its not set
|
|
||||||
func GetEnvDefault(key string, defaultValue string) string {
|
|
||||||
val, exists := os.LookupEnv(key)
|
|
||||||
if !exists {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get an environment variable as a time.Duration, specifying a default value if its
|
|
||||||
// not set or can't be parsed properly
|
|
||||||
func GetEnvDur(key string, defaultValue time.Duration) time.Duration {
|
|
||||||
val, exists := os.LookupEnv(key)
|
|
||||||
if !exists {
|
|
||||||
return time.Duration(defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
intVal, err := strconv.Atoi(val)
|
|
||||||
if err != nil {
|
|
||||||
return time.Duration(defaultValue)
|
|
||||||
}
|
|
||||||
return time.Duration(intVal)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get an environment variable as an int, specifying a default value if its
|
|
||||||
// not set or can't be parsed properly into an int
|
|
||||||
func GetEnvInt(key string, defaultValue int) int {
|
|
||||||
val, exists := os.LookupEnv(key)
|
|
||||||
if !exists {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
intVal, err := strconv.Atoi(val)
|
|
||||||
if err != nil {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return intVal
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get an environment variable as an int64, specifying a default value if its
|
|
||||||
// not set or can't be parsed properly into an int64
|
|
||||||
func GetEnvInt64(key string, defaultValue int64) int64 {
|
|
||||||
val, exists := os.LookupEnv(key)
|
|
||||||
if !exists {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
intVal, err := strconv.ParseInt(val, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return intVal
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get an environment variable as a boolean, specifying a default value if its
|
|
||||||
// not set or can't be parsed properly into a bool
|
|
||||||
func GetEnvBool(key string, defaultValue bool) bool {
|
|
||||||
val, exists := os.LookupEnv(key)
|
|
||||||
if !exists {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
truthy := map[string]bool{
|
|
||||||
"true": true, "t": true, "yes": true, "y": true, "on": true, "1": true,
|
|
||||||
"enable": true, "enabled": true, "active": true, "affirmative": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
falsy := map[string]bool{
|
|
||||||
"false": false, "f": false, "no": false, "n": false, "off": false, "0": false,
|
|
||||||
"disable": false, "disabled": false, "inactive": false, "negative": false,
|
|
||||||
}
|
|
||||||
|
|
||||||
normalized := strings.TrimSpace(strings.ToLower(val))
|
|
||||||
|
|
||||||
if val, ok := truthy[normalized]; ok {
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
if val, ok := falsy[normalized]; ok {
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
9
pkg/contexts/currentuser.go
Normal file
9
pkg/contexts/currentuser.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package contexts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"projectreshoot/internal/models"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/hwsauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
var CurrentUser hwsauth.ContextLoader[*models.User]
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package contexts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"projectreshoot/internal/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AuthenticatedUser struct {
|
|
||||||
*models.User
|
|
||||||
Fresh int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a new context with the user added in
|
|
||||||
func SetUser(ctx context.Context, u *AuthenticatedUser) context.Context {
|
|
||||||
return context.WithValue(ctx, contextKeyAuthorizedUser, u)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve a user from the given context. Returns nil if not set
|
|
||||||
func GetUser(ctx context.Context) *AuthenticatedUser {
|
|
||||||
user, ok := ctx.Value(contextKeyAuthorizedUser).(*AuthenticatedUser)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package cookies
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tell the browser to delete the cookie matching the name provided
|
|
||||||
// Path must match the original set cookie for it to delete
|
|
||||||
func DeleteCookie(w http.ResponseWriter, name string, path string) {
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: name,
|
|
||||||
Value: "",
|
|
||||||
Path: path,
|
|
||||||
Expires: time.Unix(0, 0), // Expire in the past
|
|
||||||
MaxAge: -1, // Immediately expire
|
|
||||||
HttpOnly: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a cookie with the given name, path and value. maxAge directly relates
|
|
||||||
// to cookie MaxAge (0 for no max age, >0 for TTL in seconds)
|
|
||||||
func SetCookie(
|
|
||||||
w http.ResponseWriter,
|
|
||||||
name string,
|
|
||||||
path string,
|
|
||||||
value string,
|
|
||||||
maxAge int,
|
|
||||||
) {
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: name,
|
|
||||||
Value: value,
|
|
||||||
Path: path,
|
|
||||||
HttpOnly: true,
|
|
||||||
MaxAge: maxAge,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package cookies
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check the value of "pagefrom" cookie, delete the cookie, and return the value
|
|
||||||
func CheckPageFrom(w http.ResponseWriter, r *http.Request) string {
|
|
||||||
pageFromCookie, err := r.Cookie("pagefrom")
|
|
||||||
if err != nil {
|
|
||||||
return "/"
|
|
||||||
}
|
|
||||||
pageFrom := pageFromCookie.Value
|
|
||||||
DeleteCookie(w, pageFromCookie.Name, pageFromCookie.Path)
|
|
||||||
return pageFrom
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the referer of the request, and if it matches the trustedHost, set
|
|
||||||
// the "pagefrom" cookie as the Path of the referer
|
|
||||||
func SetPageFrom(w http.ResponseWriter, r *http.Request, trustedHost string) {
|
|
||||||
referer := r.Referer()
|
|
||||||
parsedURL, err := url.Parse(referer)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var pageFrom string
|
|
||||||
if parsedURL.Path == "" || parsedURL.Host != trustedHost {
|
|
||||||
pageFrom = "/"
|
|
||||||
} else if parsedURL.Path == "/login" || parsedURL.Path == "/register" {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
pageFrom = parsedURL.Path
|
|
||||||
}
|
|
||||||
SetCookie(w, "pagefrom", "/", pageFrom, 0)
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package cookies
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"projectreshoot/internal/models"
|
|
||||||
"projectreshoot/pkg/config"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get the value of the access and refresh tokens
|
|
||||||
func GetTokenStrings(
|
|
||||||
r *http.Request,
|
|
||||||
) (acc string, ref string) {
|
|
||||||
accCookie, accErr := r.Cookie("access")
|
|
||||||
refCookie, refErr := r.Cookie("refresh")
|
|
||||||
var (
|
|
||||||
accStr string = ""
|
|
||||||
refStr string = ""
|
|
||||||
)
|
|
||||||
if accErr == nil {
|
|
||||||
accStr = accCookie.Value
|
|
||||||
}
|
|
||||||
if refErr == nil {
|
|
||||||
refStr = refCookie.Value
|
|
||||||
}
|
|
||||||
return accStr, refStr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a token with the provided details
|
|
||||||
func setToken(
|
|
||||||
w http.ResponseWriter,
|
|
||||||
config *config.Config,
|
|
||||||
token string,
|
|
||||||
scope string,
|
|
||||||
exp int64,
|
|
||||||
rememberme bool,
|
|
||||||
) {
|
|
||||||
tokenCookie := &http.Cookie{
|
|
||||||
Name: scope,
|
|
||||||
Value: token,
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
Secure: config.SSL,
|
|
||||||
}
|
|
||||||
if rememberme {
|
|
||||||
tokenCookie.Expires = time.Unix(exp, 0)
|
|
||||||
}
|
|
||||||
http.SetCookie(w, tokenCookie)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new tokens for the user and set them as cookies
|
|
||||||
func SetTokenCookies(
|
|
||||||
w http.ResponseWriter,
|
|
||||||
r *http.Request,
|
|
||||||
config *config.Config,
|
|
||||||
tokenGen *jwt.TokenGenerator,
|
|
||||||
user *models.User,
|
|
||||||
fresh bool,
|
|
||||||
rememberMe bool,
|
|
||||||
) error {
|
|
||||||
at, atexp, err := tokenGen.NewAccess(user.ID, fresh, rememberMe)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "jwt.GenerateAccessToken")
|
|
||||||
}
|
|
||||||
rt, rtexp, err := tokenGen.NewRefresh(user.ID, rememberMe)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "jwt.GenerateRefreshToken")
|
|
||||||
}
|
|
||||||
// Don't set the cookies until we know no errors occured
|
|
||||||
setToken(w, config, at, "access", atexp, rememberMe)
|
|
||||||
setToken(w, config, rt, "refresh", rtexp, rememberMe)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
--breakpoint-xl: 80rem;
|
--breakpoint-xl: 80rem;
|
||||||
--container-md: 28rem;
|
--container-md: 28rem;
|
||||||
--container-4xl: 56rem;
|
--container-4xl: 56rem;
|
||||||
|
--container-7xl: 80rem;
|
||||||
--text-xs: 0.75rem;
|
--text-xs: 0.75rem;
|
||||||
--text-xs--line-height: calc(1 / 0.75);
|
--text-xs--line-height: calc(1 / 0.75);
|
||||||
--text-sm: 0.875rem;
|
--text-sm: 0.875rem;
|
||||||
@@ -326,8 +327,8 @@
|
|||||||
.ml-0 {
|
.ml-0 {
|
||||||
margin-left: calc(var(--spacing) * 0);
|
margin-left: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.ml-\[40px\] {
|
.ml-10 {
|
||||||
margin-left: 40px;
|
margin-left: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
.ml-auto {
|
.ml-auto {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -424,6 +425,9 @@
|
|||||||
.max-w-4xl {
|
.max-w-4xl {
|
||||||
max-width: var(--container-4xl);
|
max-width: var(--container-4xl);
|
||||||
}
|
}
|
||||||
|
.max-w-7xl {
|
||||||
|
max-width: var(--container-7xl);
|
||||||
|
}
|
||||||
.max-w-100 {
|
.max-w-100 {
|
||||||
max-width: calc(var(--spacing) * 100);
|
max-width: calc(var(--spacing) * 100);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user