package server import ( "context" "crypto/tls" "errors" "go-template-webapp/internal/app" "log/slog" "net/http" "os" "os/signal" "sync" "syscall" "time" "github.com/alexedwards/scs/mysqlstore" "github.com/alexedwards/scs/sqlite3store" "github.com/alexedwards/scs/v2" "github.com/go-playground/form/v4" "github.com/go-playground/validator/v10" "github.com/jmoiron/sqlx" ) const ( defaultIdleTimeout = time.Minute defaultReadTimeout = 5 * time.Second defaultWriteTimeout = 10 * time.Second defaultShutdownPeriod = 30 * time.Second ) type server struct { logger *slog.Logger app *app.Application cfg config formDecoder *form.Decoder validate *validator.Validate sessionManager *scs.SessionManager wg sync.WaitGroup } func NewServer(logger *slog.Logger) (server, error) { //parse config cfg, err := parseCfg() if err != nil { logger.Error(err.Error()) return server{}, err } return server{logger: logger, cfg: cfg}, nil } func (srv *server) Run() error { db, err := srv.newDBConnection() if err != nil { return err } defer db.Close() srv.formDecoder = form.NewDecoder() srv.validate = validator.New(validator.WithRequiredStructEnabled()) srv.sessionManager = newSessionManager(db.DriverName, db.DB) srv.app = app.NewApp(srv.logger, db) err = srv.initApp() //Makes sure required DB fields exist. May move move DB init code here as well if err != nil { return err } return srv.serveHTTP() } func (srv *server) serveHTTP() error { tlsConfig := &tls.Config{ CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, } httpSrv := &http.Server{ Addr: srv.cfg.httpAddr, Handler: srv.routes(), ErrorLog: slog.NewLogLogger(srv.logger.Handler(), slog.LevelError), TLSConfig: tlsConfig, IdleTimeout: defaultIdleTimeout, ReadTimeout: defaultReadTimeout, WriteTimeout: defaultWriteTimeout, } shutdownErrorChan := make(chan error) go func() { quitChan := make(chan os.Signal, 1) signal.Notify(quitChan, syscall.SIGINT, syscall.SIGTERM) <-quitChan ctx, cancel := context.WithTimeout(context.Background(), defaultShutdownPeriod) defer cancel() shutdownErrorChan <- httpSrv.Shutdown(ctx) }() srv.logger.Info("starting server", slog.Group("server", "addr", httpSrv.Addr)) err := httpSrv.ListenAndServeTLS("./tls/cert.pem", "./tls/key.pem") if !errors.Is(err, http.ErrServerClosed) { return err } err = <-shutdownErrorChan if err != nil { return err } srv.logger.Info("stopped server", slog.Group("server", "addr", httpSrv.Addr)) srv.wg.Wait() return nil } func (srv *server) newDBConnection() (*database.DB, error) { db := database.New(srv.logger) err := db.Connect(srv.cfg.dsn) if err != nil { return nil, err } return db, nil } func newSessionManager(dbDriver string, db *sqlx.DB) *scs.SessionManager { sm := scs.New() if dbDriver == "mysql" { sm.Store = mysqlstore.New(db.DB) } else if dbDriver == "sqlite3" { sm.Store = sqlite3store.New(db.DB) } sm.Lifetime = 12 * time.Hour sm.Cookie.Secure = true return sm } func redirectToTls(w http.ResponseWriter, r *http.Request) { httpsUrl := "https://" + r.Host + ":6300" + r.URL.Path if r.URL.RawQuery != "" { httpsUrl += "?" + r.URL.RawQuery } http.Redirect(w, r, httpsUrl, http.StatusMovedPermanently) } // check DB tables and records, init if missing func (srv *server) initApp() error { err := srv.app.UserService.InitDB() if err != nil { return nil } return nil }