diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a943ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +config.yaml +gordio diff --git a/Makefile b/Makefile index 357bec1..70f38aa 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ -build: gordio -gordio: +all: go build -o gordio ./cmd/gordio/ + +generate: + sqlc generate -f sql/sqlc.yaml diff --git a/cmd/gordio/main.go b/cmd/gordio/main.go index b741743..4872f48 100644 --- a/cmd/gordio/main.go +++ b/cmd/gordio/main.go @@ -7,6 +7,7 @@ import ( "github.com/rs/zerolog/log" "dynatron.me/x/stillbox/pkg/gordio" + "dynatron.me/x/stillbox/pkg/gordio/admin" "dynatron.me/x/stillbox/pkg/gordio/config" "github.com/spf13/cobra" @@ -24,7 +25,8 @@ func main() { log.Fatal().Err(err).Msg("Config read failed") } - rootCmd.AddCommand(gordio.Command(cfg)) + cmds := []*cobra.Command{gordio.Command(cfg)} + rootCmd.AddCommand(append(cmds, admin.Command(cfg)...)...) err = rootCmd.Execute() if err != nil { diff --git a/config.yaml.sample b/config.yaml.sample new file mode 100644 index 0000000..297b77b --- /dev/null +++ b/config.yaml.sample @@ -0,0 +1,6 @@ +db: + driver: postgres + connect: 'postgres://postgres:password@localhost:5432/example' +jwtsecret: 'super secret string' +listen: ':3050' +public: true diff --git a/go.mod b/go.mod index 98e5d1d..b551653 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,27 @@ module dynatron.me/x/stillbox go 1.21.12 require ( + github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/httprate v0.9.0 + github.com/go-chi/jwtauth/v5 v5.3.1 + github.com/go-chi/render v1.0.3 + github.com/jackc/pgx/v5 v5.6.0 github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 + golang.org/x/crypto v0.21.0 + golang.org/x/term v0.18.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/ajg/form v1.5.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/go-chi/chi/v5 v5.1.0 // indirect - github.com/go-chi/jwtauth/v5 v5.3.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/kr/text v0.2.0 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect @@ -21,8 +32,9 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/crypto v0.21.0 // indirect golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index f5b2e19..f085f5d 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,40 @@ +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/httprate v0.9.0 h1:21A+4WDMDA5FyWcg7mNrhj63aNT8CGh+Z1alOE/piU8= +github.com/go-chi/httprate v0.9.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A= github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= @@ -30,7 +53,10 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -42,16 +68,28 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/gordio/config/config.go b/pkg/gordio/config/config.go index ce17925..074f39c 100644 --- a/pkg/gordio/config/config.go +++ b/pkg/gordio/config/config.go @@ -1,15 +1,56 @@ package config +import ( + "gopkg.in/yaml.v3" + "os" +) + type Config struct { - DB string `yaml:"db"` + DB DB `yaml:"db"` JWTSecret string `yaml:"jwtsecret"` Listen string `yaml:"listen"` Public bool `yaml:"public"` } -func ReadConfig() (*Config, error) { - return &Config{ - JWTSecret: "s3c4r", - Listen: ":3050", - }, nil +type DB struct { + Connect string `yaml:"connect"` + Driver string `yaml:"driver"` +} + +type ConfigOption func(*configOptions) + +type configOptions struct { + configPath string +} + +func WithConfigPath(p string) ConfigOption { + return func(o *configOptions) { + o.configPath = p + } +} + +func ReadConfig(opts ...ConfigOption) (*Config, error) { + o := new(configOptions) + + for _, opt := range opts { + opt(o) + } + + if o.configPath == "" { + o.configPath = "config.yaml" + } + + cfgBytes, err := os.ReadFile(o.configPath) + if err != nil { + return nil, err + } + + c := new(Config) + + err = yaml.Unmarshal(cfgBytes, c) + if err != nil { + return nil, err + } + + return c, nil } diff --git a/pkg/gordio/server/routes.go b/pkg/gordio/server/routes.go index 70e35c8..107225d 100644 --- a/pkg/gordio/server/routes.go +++ b/pkg/gordio/server/routes.go @@ -1,11 +1,13 @@ package server import ( - "fmt" "net/http" + "time" "github.com/go-chi/chi/v5" + "github.com/go-chi/httprate" "github.com/go-chi/jwtauth/v5" + "github.com/go-chi/render" ) func (s *Server) setupRoutes() { @@ -17,11 +19,15 @@ func (s *Server) setupRoutes() { }) - r.Group(func (r chi.Router) { + r.Group(func(r chi.Router) { + r.Use(rateLimiter()) + r.Use(render.SetContentType(render.ContentTypeJSON)) // public routes + r.Post("/auth", s.routeAuth) }) r.Group(func(r chi.Router) { + r.Use(rateLimiter()) r.Use(jwtauth.Verifier(s.jwt)) // optional auth routes @@ -30,15 +36,39 @@ func (s *Server) setupRoutes() { }) } -func (s *Server) routeIndex(w http.ResponseWriter, r *http.Request) { - if s.Authenticated(r) { - w.Write([]byte(fmt.Sprint("Welcome\n"))) - // error - } +func rateLimiter() func(http.Handler) http.Handler { + return httprate.LimitByRealIP(100, 1*time.Minute) } -func (s *Server) Authenticated(r *http.Request) bool { - // TODO: check IP against ACL, or conf.Public, and against map of routes - tok, _, _ := jwtauth.FromContext(r.Context()) - return tok != nil +func (s *Server) routeIndex(w http.ResponseWriter, r *http.Request) { + if cl, authenticated := s.Authenticated(r); authenticated { + w.Write([]byte("Hello " + cl["user"].(string) + "\n")) + } + w.Write([]byte("Welcome to gordio\n")) +} + +func (s *Server) routeAuth(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + username, password := r.PostFormValue("username"), r.PostFormValue("password") + if username == "" || password == "" { + http.Error(w, "blank credentials", http.StatusBadRequest) + return + } + + tok, err := s.Login(r.Context(), username, password) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + jr := struct { + JWT string `json:"jwt"` + }{ + JWT: tok, + } + render.JSON(w, r, &jr) } diff --git a/pkg/gordio/server/server.go b/pkg/gordio/server/server.go index d3023c7..1f882fe 100644 --- a/pkg/gordio/server/server.go +++ b/pkg/gordio/server/server.go @@ -1,33 +1,37 @@ package server import ( - "log" + "context" "net/http" "dynatron.me/x/stillbox/pkg/gordio/config" + "dynatron.me/x/stillbox/pkg/gordio/database" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/jwtauth/v5" + "github.com/jackc/pgx/v5" ) type Server struct { conf *config.Config + db *pgx.Conn r *chi.Mux jwt *jwtauth.JWTAuth } func New(cfg *config.Config) (*Server, error) { + db, err := database.NewClient(cfg.DB) + if err != nil { + return nil, err + } + r := chi.NewRouter() srv := &Server{ conf: cfg, + db: db, r: r, jwt: jwtauth.New("HS256", []byte(cfg.JWTSecret), nil), } - _, tokenString, err := srv.jwt.Encode(map[string]interface{}{"user_id": 123}) - if err != nil { - panic(err) - } - log.Printf("DEBUG token is %s", tokenString) r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) @@ -38,7 +42,8 @@ func New(cfg *config.Config) (*Server, error) { } func (s *Server) Go() error { + defer s.db.Close(context.Background()) + http.ListenAndServe(s.conf.Listen, s.r) return nil } - diff --git a/sql/postgres/migrations/001_initial.down.sql b/sql/postgres/migrations/001_initial.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/sql/postgres/migrations/001_initial.up.sql b/sql/postgres/migrations/001_initial.up.sql new file mode 100644 index 0000000..05b352b --- /dev/null +++ b/sql/postgres/migrations/001_initial.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS users( + id SERIAL PRIMARY KEY, + username VARCHAR (255) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + is_admin BOOLEAN +); + diff --git a/sql/postgres/query.sql b/sql/postgres/query.sql new file mode 100644 index 0000000..15323c2 --- /dev/null +++ b/sql/postgres/query.sql @@ -0,0 +1,19 @@ +-- name: GetUserByID :one +SELECT * FROM users +WHERE id = $1 LIMIT 1; + +-- name: GetUserByUsername :one +SELECT * FROM users +WHERE username = $1 LIMIT 1; + +-- name: GetUsers :many +SELECT * FROM users; + +-- name: CreateUser :one +INSERT INTO users ( + username, + password, + email, + is_admin + ) VALUES ($1, $2, $3, $4) +RETURNING *; diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml new file mode 100644 index 0000000..1e347b3 --- /dev/null +++ b/sql/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "postgres/query.sql" + schema: "postgres/migrations" + gen: + go: + package: "database" + out: "../pkg/gordio/database" + sql_package: "pgx/v5"