From 2a8566712a7f8bf549a0b2f8db342ac8c4cabcd4 Mon Sep 17 00:00:00 2001 From: Vadim Date: Wed, 12 Nov 2025 09:41:52 +0300 Subject: [PATCH] init --- .gitignore | 33 +++++ Makefile | 20 +++ README.md | 1 + cmd/grpc.go | 43 ++++++ cmd/main.go | 69 +++++++++ config/example.config.yaml | 10 ++ go.mod | 41 ++++++ go.sum | 124 ++++++++++++++++ internal/broker/broker.go | 23 +++ internal/config/config.go | 42 ++++++ internal/database/client.go | 30 ++++ internal/helpers/helpers.go | 12 ++ internal/logger/logger.go | 10 ++ internal/model/auth/auth.go | 12 ++ internal/model/issue/issue.go | 32 +++++ internal/model/project/project.go | 23 +++ internal/model/setting/setting.go | 7 + internal/model/status/status.go | 13 ++ internal/model/user/user.go | 27 ++++ internal/server/auth.go | 150 ++++++++++++++++++++ internal/server/health-check.go | 16 +++ internal/server/helpers/helpers.go | 63 +++++++++ internal/server/issue.go | 104 ++++++++++++++ internal/server/middleware.go | 57 ++++++++ internal/server/profile.go | 48 +++++++ internal/server/project.go | 147 +++++++++++++++++++ internal/server/request/request.go | 21 +++ internal/server/response/response.go | 60 ++++++++ internal/server/server-settings.go | 15 ++ internal/server/server.go | 157 +++++++++++++++++++++ internal/server/status.go | 78 +++++++++++ internal/server/user.go | 7 + internal/storage/issues.go | 202 +++++++++++++++++++++++++++ internal/storage/projects.go | 169 ++++++++++++++++++++++ internal/storage/server-settings.go | 46 ++++++ internal/storage/status.go | 99 +++++++++++++ internal/storage/storage.go | 25 ++++ internal/storage/user-to-project.go | 32 +++++ internal/storage/user.go | 108 ++++++++++++++ proto/hello.pb.go | 180 ++++++++++++++++++++++++ proto/hello.proto | 17 +++ proto/hello_grpc.pb.go | 121 ++++++++++++++++ sql/down.sql | 8 ++ sql/up.sql | 100 +++++++++++++ 44 files changed, 2602 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/grpc.go create mode 100644 cmd/main.go create mode 100644 config/example.config.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/broker/broker.go create mode 100644 internal/config/config.go create mode 100644 internal/database/client.go create mode 100644 internal/helpers/helpers.go create mode 100644 internal/logger/logger.go create mode 100644 internal/model/auth/auth.go create mode 100644 internal/model/issue/issue.go create mode 100644 internal/model/project/project.go create mode 100644 internal/model/setting/setting.go create mode 100644 internal/model/status/status.go create mode 100644 internal/model/user/user.go create mode 100644 internal/server/auth.go create mode 100644 internal/server/health-check.go create mode 100644 internal/server/helpers/helpers.go create mode 100644 internal/server/issue.go create mode 100644 internal/server/middleware.go create mode 100644 internal/server/profile.go create mode 100644 internal/server/project.go create mode 100644 internal/server/request/request.go create mode 100644 internal/server/response/response.go create mode 100644 internal/server/server-settings.go create mode 100644 internal/server/server.go create mode 100644 internal/server/status.go create mode 100644 internal/server/user.go create mode 100644 internal/storage/issues.go create mode 100644 internal/storage/projects.go create mode 100644 internal/storage/server-settings.go create mode 100644 internal/storage/status.go create mode 100644 internal/storage/storage.go create mode 100644 internal/storage/user-to-project.go create mode 100644 internal/storage/user.go create mode 100644 proto/hello.pb.go create mode 100644 proto/hello.proto create mode 100644 proto/hello_grpc.pb.go create mode 100644 sql/down.sql create mode 100644 sql/up.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b293f6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo +/bin/ +/web/* +/config/config.yaml diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fc3e6e6 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +BINARY_NAME=server +MAIN_PACKAGE_PATH = ./cmd/main.go + +.PHONY: tidy +tidy: + go mod tidy -v + go fmt ./... + +.PHONY: build +build: + go build -o bin/${BINARY_NAME} ${MAIN_PACKAGE_PATH} + # GOARCH=arm64 GOOS=darwin go build -o bin/${BINARY_NAME} ${MAIN_PACKAGE_PATH} + +.PHONY: run +run: build + ./bin/${BINARY_NAME} + +.PHONY: gen +gen: + protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/hello.proto \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7c17d9 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Hello world \ No newline at end of file diff --git a/cmd/grpc.go b/cmd/grpc.go new file mode 100644 index 0000000..a145888 --- /dev/null +++ b/cmd/grpc.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + "flag" + "fmt" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + "log" + pb "madsky.ru/go-tracker/proto" + "net" +) + +var port = flag.Int("port", 50051, "the server port") + +type GrpcServer struct { + pb.UnimplementedHelloServer +} + +func (s *GrpcServer) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { + log.Printf("Received: %v", in.GetName()) + return &pb.HelloResponse{Message: "Hello " + in.GetName()}, nil +} + +func grpcServer() { + flag.Parse() + + listen, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + s := grpc.NewServer() + + reflection.Register(s) + pb.RegisterHelloServer(s, &GrpcServer{}) + + log.Printf("server listening at %v", listen.Addr()) + + if err := s.Serve(listen); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..2b04f3c --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "fmt" + "github.com/nats-io/nats.go" + "madsky.ru/go-tracker/internal/broker" + "madsky.ru/go-tracker/internal/config" + "madsky.ru/go-tracker/internal/database" + "madsky.ru/go-tracker/internal/logger" + "madsky.ru/go-tracker/internal/server" + "madsky.ru/go-tracker/internal/storage" + "os" +) + +func main() { + cfg := config.MustLoad("./config/config.yaml") + + log := logger.NewLogger() + dsn := getDSN(cfg.Database.Host, cfg.Database.Port, cfg.Database.User, cfg.Database.Password, cfg.Database.Name) + + //ctx := context.WithValue(context.Background(), "value", "foo bar") + //ctx := context.Background() + ctx, cancel := context.WithCancel(context.WithValue(context.Background(), "value", "foo bar")) + defer cancel() + + // broker section + n := broker.NewBroker(cfg) + defer n.Close() + + pubErr := n.Publish("test", []byte("foobar")) + if pubErr != nil { + fmt.Println(pubErr) + } + _, subErr := n.Subscribe("test", func(msg *nats.Msg) { + fmt.Println(msg.Subject, string(msg.Data)) + }) + if subErr != nil { + log.Error("Error subscribing", subErr) + } + // end broker section + + client, err := database.NewClient(ctx, dsn, log) + if err != nil { + log.Error("Error connecting to database", err) + os.Exit(1) + } + defer client.Close() + + store := storage.NewStorage(client) + + app := &server.Application{ + Config: cfg, + Storage: store, + Ctx: ctx, + } + r := app.Routes() + + if err = app.Start(r); err != nil { + log.Error("Error starting http server", err) + os.Exit(1) + } + +} + +func getDSN(host, port, user, password, name string) string { + return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, name) +} diff --git a/config/example.config.yaml b/config/example.config.yaml new file mode 100644 index 0000000..8cf8a5c --- /dev/null +++ b/config/example.config.yaml @@ -0,0 +1,10 @@ +app_name: 'APP' +nats: + host: '' + token: '' +database: + host: '' + port: 5432 + user: '' + password: '' + name: 'tracker' \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3d9016e --- /dev/null +++ b/go.mod @@ -0,0 +1,41 @@ +module madsky.ru/go-tracker + +go 1.25.4 + +require ( + github.com/go-chi/chi/v5 v5.2.3 + github.com/go-playground/validator/v10 v10.28.0 + github.com/goccy/go-json v0.10.5 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/jackc/pgx/v5 v5.7.6 + github.com/nats-io/nats.go v1.47.0 + golang.org/x/crypto v0.44.0 + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.11 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/stretchr/testify v1.11.1 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ef2c072 --- /dev/null +++ b/go.sum @@ -0,0 +1,124 @@ +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +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/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= +github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/internal/broker/broker.go b/internal/broker/broker.go new file mode 100644 index 0000000..f2352a7 --- /dev/null +++ b/internal/broker/broker.go @@ -0,0 +1,23 @@ +package broker + +import ( + "fmt" + "github.com/nats-io/nats.go" + "madsky.ru/go-tracker/internal/config" +) + +func NewBroker(cfg *config.Config) *nats.Conn { + if cfg.Nats.Token == "" { + panic("NATS Token not set") + } + + url := fmt.Sprintf("nats://%s@%s:%s", cfg.Nats.Token, cfg.Nats.Host, cfg.Nats.Port) + + nc, err := nats.Connect(url) + if err != nil { + fmt.Println(err) + panic(err) + } + + return nc +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..96415e6 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,42 @@ +package config + +import ( + "github.com/ilyakaznacheev/cleanenv" +) + +type Config struct { + AppName string `yaml:"app_name" env:"APP_NAME" env-default:"test"` + Server Server `yaml:"server"` + Database Database `yaml:"database"` + Nats Nats `yaml:"nats"` +} + +type Server struct { + Port string `yaml:"port" env:"PORT" env-default:"3000"` + Host string `yaml:"host" env:"HOST" env-default:"localhost"` +} + +type Nats struct { + Host string `yaml:"host" env:"NATS_HOST" env-default:"localhost"` + Port string `yaml:"port" env:"NATS_PORT" env-default:"4222"` + Token string `yaml:"token"` +} + +type Database struct { + Host string `yaml:"host"` + Port string `yaml:"port"` + User string `yaml:"user"` + Password string `yaml:"password"` + Name string `yaml:"name"` +} + +func MustLoad(path string) *Config { + var config Config + + err := cleanenv.ReadConfig(path, &config) + if err != nil { + panic(err) + } + + return &config +} diff --git a/internal/database/client.go b/internal/database/client.go new file mode 100644 index 0000000..7bb83de --- /dev/null +++ b/internal/database/client.go @@ -0,0 +1,30 @@ +package database + +import ( + "context" + "fmt" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" + "log/slog" + "os" +) + +type Client interface { + Exec(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error) + Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) + QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row + Begin(ctx context.Context) (pgx.Tx, error) + Ping(ctx context.Context) error +} + +func NewClient(ctx context.Context, connUrl string, logger *slog.Logger) (*pgxpool.Pool, error) { + pool, err := pgxpool.New(ctx, connUrl) + if err != nil { + fmt.Println(fmt.Fprintf(os.Stderr, "Unable to create connection pool: %v\n", err)) + logger.Error("Unable to create connection pool: ", err) + os.Exit(1) + } + + return pool, nil +} diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go new file mode 100644 index 0000000..48fa084 --- /dev/null +++ b/internal/helpers/helpers.go @@ -0,0 +1,12 @@ +package helpers + +import ( + "fmt" +) + +func PrintData(label string, data interface{}) { + fmt.Printf("---%s %#v\n", label, data) + //fmt.Printf("*** CONFIG %+v\n", app.Config) +} + +// const connUrl = "postgres://postgres:postgres@localhost:5432/go-finance" diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..9174539 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,10 @@ +package logger + +import ( + "log/slog" + "os" +) + +func NewLogger() *slog.Logger { + return slog.New(slog.NewJSONHandler(os.Stdout, nil)) +} diff --git a/internal/model/auth/auth.go b/internal/model/auth/auth.go new file mode 100644 index 0000000..d317ec1 --- /dev/null +++ b/internal/model/auth/auth.go @@ -0,0 +1,12 @@ +package auth + +type LoginDTO struct { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` +} + +type RegisterDTO struct { + Email string `json:"email" validate:"required,email"` + Name string `json:"name" validate:"required"` + Password string `json:"password" validate:"required"` +} diff --git a/internal/model/issue/issue.go b/internal/model/issue/issue.go new file mode 100644 index 0000000..1f9b952 --- /dev/null +++ b/internal/model/issue/issue.go @@ -0,0 +1,32 @@ +package issue + +import ( + "madsky.ru/go-tracker/internal/model/project" + "madsky.ru/go-tracker/internal/model/status" + "time" +) + +type Issue struct { + ID uint32 `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Position uint32 `json:"position"` + Created time.Time `json:"created"` + StatusID uint32 `json:"status_id"` + ProjectID uint32 `json:"project_id"` + Status status.Status `json:"status"` + Project project.Project `json:"project"` +} + +type CreateIssueDTO struct { + Name string `json:"name"` + Description string `json:"description"` + StatusID uint32 `json:"status_id"` + ProjectID uint32 `json:"project_id"` + Position *uint32 `json:"position"` +} + +type PositionDTO struct { + StatusId uint32 `json:"status_id"` + Positions []uint32 `json:"positions"` +} diff --git a/internal/model/project/project.go b/internal/model/project/project.go new file mode 100644 index 0000000..e2880b6 --- /dev/null +++ b/internal/model/project/project.go @@ -0,0 +1,23 @@ +package project + +type Project struct { + ID uint32 `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Key string `json:"key"` +} + +type CreateProjectDTO struct { + Name string `json:"name" validate:"required"` + Description string `json:"description"` + Key string `json:"key"` +} + +type UpdateProjectDTO struct { + Name string `json:"name"` + Description string `json:"description"` +} + +type FilterDTO struct { + UserID *uint32 `json:"user_id"` +} diff --git a/internal/model/setting/setting.go b/internal/model/setting/setting.go new file mode 100644 index 0000000..f228211 --- /dev/null +++ b/internal/model/setting/setting.go @@ -0,0 +1,7 @@ +package setting + +type Setting struct { + ID uint32 `json:"id"` + Name string `json:"name"` + Value string `json:"value"` +} diff --git a/internal/model/status/status.go b/internal/model/status/status.go new file mode 100644 index 0000000..12d34b0 --- /dev/null +++ b/internal/model/status/status.go @@ -0,0 +1,13 @@ +package status + +type Status struct { + ID uint32 `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Position uint32 `json:"position"` +} + +type CreateStatusDTO struct { + Name string `json:"name"` + Description string `json:"description"` +} diff --git a/internal/model/user/user.go b/internal/model/user/user.go new file mode 100644 index 0000000..8fd4f73 --- /dev/null +++ b/internal/model/user/user.go @@ -0,0 +1,27 @@ +package user + +type User struct { + ID uint32 `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + PasswordHash string `json:"password_hash"` + Role string `json:"role"` +} + +type CreateUserDTO struct { + Email string `json:"email"` + Name string `json:"name"` + PasswordHash string `json:"password_hash"` + Role string `json:"role"` +} + +type UpdateUserDTO struct { + Name string `json:"name"` +} + +type ResponseDTO struct { + ID uint32 `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + Role string `json:"role"` +} diff --git a/internal/server/auth.go b/internal/server/auth.go new file mode 100644 index 0000000..221b4e4 --- /dev/null +++ b/internal/server/auth.go @@ -0,0 +1,150 @@ +package server + +import ( + "errors" + "fmt" + "github.com/go-playground/validator/v10" + "github.com/goccy/go-json" + "madsky.ru/go-tracker/internal/model/auth" + "madsky.ru/go-tracker/internal/model/user" + "madsky.ru/go-tracker/internal/server/helpers" + "madsky.ru/go-tracker/internal/server/response" + "net/http" + "strings" + "time" +) + +const TTL = 12 + +var validate *validator.Validate + +func (app *Application) login(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + var loginDto auth.LoginDTO + + if err := dec.Decode(&loginDto); err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + validate = validator.New(validator.WithRequiredStructEnabled()) + err := validate.Struct(loginDto) + if err != nil { + var validateErrs validator.ValidationErrors + + errorsMessages := make([]string, 0) + if errors.As(err, &validateErrs) { + for _, e := range validateErrs { + errorsMessages = append(errorsMessages, fmt.Sprintf("%s is %s", e.Field(), e.Tag())) + } + } + + response.Error(w, errors.New(strings.Join(errorsMessages, ",")), http.StatusBadRequest) + return + } + + u, err := app.Storage.User.FindByEmail(app.Ctx, loginDto.Email) + if err != nil { + response.Error(w, errors.New("user does not exist"), http.StatusBadRequest) + return + } + + if !helpers.VerifyHashString(loginDto.Password, u.PasswordHash) { + response.Error(w, errors.New("wrong password"), http.StatusBadRequest) + return + } + + expires := time.Now().Add(time.Hour * TTL) + token, err := helpers.CreateToken(u.ID, u.Role, expires) + if err != nil { + response.Error(w, err, http.StatusInternalServerError) + return + } + + data := make(map[string]string) + data["token"] = token + data["exp"] = expires.UTC().Format(time.RFC3339) + + cookie := &http.Cookie{ + Name: "token", + Path: "/", + Value: token, + Expires: expires, + HttpOnly: true, + } + http.SetCookie(w, cookie) + + response.WriteJSON(w, nil, http.StatusCreated, data) +} + +func (app *Application) register(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + var registerDto auth.RegisterDTO + + if err := dec.Decode(®isterDto); err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + validate = validator.New(validator.WithRequiredStructEnabled()) + err := validate.Struct(registerDto) + if err != nil { + var validateErrs validator.ValidationErrors + + errorsMessages := make([]string, 0) + if errors.As(err, &validateErrs) { + for _, e := range validateErrs { + errorsMessages = append(errorsMessages, fmt.Sprintf("%s is %s", e.Field(), e.Tag())) + } + } + + response.Error(w, errors.New(strings.Join(errorsMessages, ",")), http.StatusBadRequest) + return + } + + isEmpty, err := app.Storage.User.IsEmpty(app.Ctx) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + var newUser user.User + + passwordHash, err := helpers.CreateHashString(registerDto.Password) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + newUser.Email = registerDto.Email + newUser.Name = registerDto.Name + newUser.PasswordHash = passwordHash + if isEmpty { + newUser.Role = "admin" + } else { + newUser.Role = "user" + } + res, err := app.Storage.User.Create(app.Ctx, &newUser) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, response.ToUserResponseDto(res)) + +} + +func (app *Application) logout(w http.ResponseWriter, _ *http.Request) { + cookie := &http.Cookie{ + Name: "token", + Value: "", + Path: "/", + MaxAge: -1, + } + http.SetCookie(w, cookie) + response.WriteJSON(w, nil, http.StatusCreated, 1) +} diff --git a/internal/server/health-check.go b/internal/server/health-check.go new file mode 100644 index 0000000..8c4b271 --- /dev/null +++ b/internal/server/health-check.go @@ -0,0 +1,16 @@ +package server + +import ( + "madsky.ru/go-tracker/internal/server/response" + "net/http" +) + +func (app *Application) getVersion(w http.ResponseWriter, r *http.Request) { + s, err := app.Storage.ServerSettings.Find(app.Ctx) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, &s) +} diff --git a/internal/server/helpers/helpers.go b/internal/server/helpers/helpers.go new file mode 100644 index 0000000..fd27f2e --- /dev/null +++ b/internal/server/helpers/helpers.go @@ -0,0 +1,63 @@ +package helpers + +import ( + "errors" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" + "time" +) + +var secretKey = []byte("1234567890") + +type TokenClaims struct { + UserId uint32 `json:"user_id"` + Role string `json:"role"` + Expires int64 `json:"expires"` + jwt.RegisteredClaims +} + +func CreateToken(userId uint32, role string, expires time.Time) (string, error) { + claims := TokenClaims{ + UserId: userId, + Role: role, + Expires: expires.Unix(), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expires), + Issuer: "https://madsky.ru", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + ss, err := token.SignedString(secretKey) + if err != nil { + return "", err + } + + return ss, nil +} + +func VerifyToken(tokenString string) (*TokenClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) { + return secretKey, nil + }) + + if err != nil { + return nil, err + } else if claims, ok := token.Claims.(*TokenClaims); ok && token.Valid { + return claims, nil + } else { + return nil, errors.New("unknown claims type, cannot proceed") + } + +} + +func CreateHashString(password string) (string, error) { + cost := bcrypt.DefaultCost + bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost) + return string(bytes), err +} + +func VerifyHashString(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/internal/server/issue.go b/internal/server/issue.go new file mode 100644 index 0000000..b01e9c9 --- /dev/null +++ b/internal/server/issue.go @@ -0,0 +1,104 @@ +package server + +import ( + "github.com/goccy/go-json" + "madsky.ru/go-tracker/internal/model/issue" + "madsky.ru/go-tracker/internal/server/request" + "madsky.ru/go-tracker/internal/server/response" + "net/http" +) + +func (app *Application) findIssues(w http.ResponseWriter, _ *http.Request) { + i, err := app.Storage.Issues.Find(app.Ctx) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, &i) +} + +func (app *Application) findIssuesByID(w http.ResponseWriter, r *http.Request) { + id, err := request.Param(r, "id") + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + i, err := app.Storage.Issues.FindOne(app.Ctx, id) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, &i) +} + +func (app *Application) createIssues(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + var issueDto issue.CreateIssueDTO + + err := dec.Decode(&issueDto) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + id, err := app.Storage.Issues.Create(app.Ctx, &issueDto) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + i, err := app.Storage.Issues.FindOne(app.Ctx, *id) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, &i) +} + +func (app *Application) updatePositions(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + var positionDto issue.PositionDTO + + err := dec.Decode(&positionDto) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + i := make([]*uint32, 0) + + for position, issueId := range positionDto.Positions { + id, err := app.Storage.Issues.UpdatePositions(app.Ctx, position, positionDto.StatusId, issueId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + i = append(i, id) + } + + response.WriteJSON(w, nil, http.StatusOK, &i) +} + +func (app *Application) deleteIssues(w http.ResponseWriter, r *http.Request) { + id, err := request.Param(r, "id") + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + count, err := app.Storage.Issues.Remove(app.Ctx, id) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, count) +} diff --git a/internal/server/middleware.go b/internal/server/middleware.go new file mode 100644 index 0000000..40394d2 --- /dev/null +++ b/internal/server/middleware.go @@ -0,0 +1,57 @@ +package server + +import ( + "context" + "errors" + "fmt" + "madsky.ru/go-tracker/internal/model/user" + "madsky.ru/go-tracker/internal/server/helpers" + "madsky.ru/go-tracker/internal/server/response" + "net/http" +) + +type UserContext struct { +} + +func (app *Application) AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("token") + if err != nil { + switch { + case errors.Is(err, http.ErrNoCookie): + response.Error(w, errors.New("no authentication token"), http.StatusForbidden) + default: + response.Error(w, errors.New("unknown error"), http.StatusForbidden) + } + return + } + + claims, err := helpers.VerifyToken(cookie.Value) + if err != nil { + response.Error(w, errors.New("invalid authentication token"), http.StatusForbidden) + return + } + + u, err := app.Storage.User.FindById(app.Ctx, claims.UserId) + if err != nil { + response.Error(w, errors.New("user not found"), http.StatusBadRequest) + return + } + ctx := context.WithValue(r.Context(), UserContext{}, u) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + +} + +func (app *Application) LogRole(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u := ctx.Value(UserContext{}).(*user.User) + + isAdmin := u.Role == "admin" + + fmt.Println(fmt.Sprintf("is admin: %v", isAdmin)) + + next.ServeHTTP(w, r) + }) +} diff --git a/internal/server/profile.go b/internal/server/profile.go new file mode 100644 index 0000000..04a1197 --- /dev/null +++ b/internal/server/profile.go @@ -0,0 +1,48 @@ +package server + +import ( + "fmt" + "github.com/goccy/go-json" + "madsky.ru/go-tracker/internal/model/user" + "madsky.ru/go-tracker/internal/server/response" + "net/http" +) + +func (app *Application) getProfile(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + res := ctx.Value(UserContext{}).(*user.User) + + response.WriteJSON(w, nil, http.StatusOK, response.ToUserResponseDto(res)) +} + +func (app *Application) updateProfile(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u := ctx.Value(UserContext{}).(*user.User) + + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + var dto user.UpdateUserDTO + + if err := dec.Decode(&dto); err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + res, err := app.Storage.User.Update(app.Ctx, u.ID, &dto) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, response.ToUserResponseDto(res)) +} + +func (app *Application) deleteProfile(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u := ctx.Value(UserContext{}).(*user.User) + + fmt.Println(u) + + response.WriteJSON(w, nil, http.StatusOK, nil) +} diff --git a/internal/server/project.go b/internal/server/project.go new file mode 100644 index 0000000..7e1df40 --- /dev/null +++ b/internal/server/project.go @@ -0,0 +1,147 @@ +package server + +import ( + "errors" + "fmt" + "github.com/go-playground/validator/v10" + "github.com/goccy/go-json" + "madsky.ru/go-tracker/internal/model/project" + "madsky.ru/go-tracker/internal/model/user" + "madsky.ru/go-tracker/internal/server/request" + "madsky.ru/go-tracker/internal/server/response" + "net/http" + "strings" +) + +func (app *Application) findProjects(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u := ctx.Value(UserContext{}).(*user.User) + + var filter project.FilterDTO + + if u.Role != "admin" { + filter.UserID = &u.ID + } + + isAdmin := u.Role == "admin" + + p, err := app.Storage.Projects.Find(app.Ctx, u.ID, isAdmin, &filter) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, &p) +} + +func (app *Application) findProjectByID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u := ctx.Value(UserContext{}).(*user.User) + + projectId, err := request.Param(r, "id") + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + var filter project.FilterDTO + + if u.Role != "admin" { + filter.UserID = &u.ID + } + + isAdmin := u.Role == "admin" + + p, err := app.Storage.Projects.FindOne(app.Ctx, projectId, u.ID, isAdmin) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + response.WriteJSON(w, nil, http.StatusOK, &p) +} + +func (app *Application) createProject(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u := ctx.Value(UserContext{}).(*user.User) + + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + var projectDto project.CreateProjectDTO + + if err := dec.Decode(&projectDto); err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + validate = validator.New(validator.WithRequiredStructEnabled()) + err := validate.Struct(projectDto) + if err != nil { + var validateErrs validator.ValidationErrors + + errorsMessages := make([]string, 0) + if errors.As(err, &validateErrs) { + for _, e := range validateErrs { + errorsMessages = append(errorsMessages, fmt.Sprintf("%s is %s", e.Field(), e.Tag())) + } + } + + response.Error(w, errors.New(strings.Join(errorsMessages, ",")), http.StatusBadRequest) + return + } + + p, err := app.Storage.Projects.Create(app.Ctx, &projectDto) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + err = app.Storage.UserToProject.Create(app.Ctx, u.ID, p.ID) + if err != nil { + response.Error(w, err, http.StatusInternalServerError) + } + + response.WriteJSON(w, nil, http.StatusCreated, &p) +} + +func (app *Application) updateProject(w http.ResponseWriter, r *http.Request) { + id, err := request.Param(r, "id") + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + var projectDto project.UpdateProjectDTO + + if err = dec.Decode(&projectDto); err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + p, err := app.Storage.Projects.Update(app.Ctx, id, &projectDto) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, &p) +} + +func (app *Application) deleteProject(w http.ResponseWriter, r *http.Request) { + id, err := request.Param(r, "id") + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + count, err := app.Storage.Projects.Remove(app.Ctx, id) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, count) +} diff --git a/internal/server/request/request.go b/internal/server/request/request.go new file mode 100644 index 0000000..e42841c --- /dev/null +++ b/internal/server/request/request.go @@ -0,0 +1,21 @@ +package request + +import ( + "net/http" + "strconv" +) + +func Param(r *http.Request, name string) (uint64, error) { + idStr := r.PathValue(name) + id, err := strconv.ParseUint(idStr, 10, 64) + + if err != nil { + return id, err + } + + if id < 1 { + return id, err + } + + return id, nil +} diff --git a/internal/server/response/response.go b/internal/server/response/response.go new file mode 100644 index 0000000..e978af6 --- /dev/null +++ b/internal/server/response/response.go @@ -0,0 +1,60 @@ +package response + +import ( + "fmt" + "github.com/goccy/go-json" + "madsky.ru/go-tracker/internal/model/user" + "net/http" +) + +type Response struct { + Status string `json:"status"` + Message *string `json:"message"` + Data interface{} `json:"data"` +} + +const ( + StatusOK = "Success" + StatusError = "Error" +) + +func WriteJSON(w http.ResponseWriter, message *string, code int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + + statusText := StatusOK + if code >= 400 { + statusText = StatusError + } + + if err := json.NewEncoder(w).Encode(&Response{ + Status: statusText, + Message: message, + Data: data, + }); err != nil { + http.Error(w, "unknown error", http.StatusInternalServerError) + } +} + +type CustomError struct { + Message string +} + +func (e *CustomError) Error() string { + return fmt.Sprintf("custom error: %s", e.Message) +} + +func Error(w http.ResponseWriter, err error, code int) { + errorMessage := err.Error() + WriteJSON(w, &errorMessage, code, nil) +} + +func ToUserResponseDto(u *user.User) *user.ResponseDTO { + var ur user.ResponseDTO + ur.ID = u.ID + ur.Email = u.Email + ur.Role = u.Role + ur.Name = u.Name + + return &ur +} diff --git a/internal/server/server-settings.go b/internal/server/server-settings.go new file mode 100644 index 0000000..fccf60e --- /dev/null +++ b/internal/server/server-settings.go @@ -0,0 +1,15 @@ +package server + +import ( + "fmt" + "madsky.ru/go-tracker/internal/model/user" + "net/http" +) + +func (app *Application) getSettings(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u := ctx.Value(UserContext{}).(*user.User) + + fmt.Println(u) + return +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..682d0c2 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,157 @@ +package server + +import ( + "context" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/jackc/pgx/v5/pgxpool" + "io/fs" + "log/slog" + "madsky.ru/go-tracker/internal/config" + "madsky.ru/go-tracker/internal/storage" + web "madsky.ru/go-tracker/web" + "net/http" + "os" + "strings" + "time" +) + +type Application struct { + Config *config.Config + Storage *storage.Storage + Ctx context.Context +} + +func NewServer(ctx context.Context, client *pgxpool.Pool, logger *slog.Logger) { + //statusRepository := status.NewRepository(client) + //projectsRepository := project.NewRepository(client) + //issueRepository := issue.NewRepository(client) + //userRepository := user.NewRepository(client) + + //r.Get("/bla/assets/*", func(w http.ResponseWriter, r *http.Request) { + // dist, err := fs.Sub(web.DistDir, "dist") + // if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // } + // http.FileServer(http.FS(dist)).ServeHTTP(w, r) + //}) + + //handlers.RegisterAuthRoutes(mux, ctx, userRepository) + // + //authMux := http.NewServeMux() + //handlers.RegisterProjectRoutes(authMux, ctx, projectsRepository) + //handlers.RegisterStatusRoutes(authMux, ctx, statusRepository) + //handlers.RegisterIssueRoutes(authMux, ctx, issueRepository) + //handlers.RegisterUserRoutes(authMux, ctx, userRepository) + // + //mux.Handle("/api/", http.StripPrefix("/api", md.AuthMiddleware(authMux))) + // + //handler := md.LoggingMiddleware(mux, logger) +} + +func (app *Application) Routes() http.Handler { + r := chi.NewRouter() + + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + r.Route("/api", func(r chi.Router) { + r.Route("/version", func(r chi.Router) { + r.Get("/", app.getVersion) + }) + + r.Route("/auth", func(r chi.Router) { + r.Post("/login", app.login) + r.Post("/register", app.register) + r.Post("/logout", app.logout) + }) + + r.Route("/projects", func(r chi.Router) { + r.Use(app.AuthMiddleware) + r.Use(app.LogRole) + r.Get("/", app.findProjects) + r.Post("/", app.createProject) + + r.Route("/{id}", func(r chi.Router) { + r.Get("/", app.findProjectByID) + r.Patch("/", app.updateProject) + r.Delete("/", app.deleteProject) + }) + }) + + r.Route("/statuses", func(r chi.Router) { + r.Use(app.AuthMiddleware) + r.Get("/", app.findStatuses) + }) + + r.Route("/issues", func(r chi.Router) { + r.Use(app.AuthMiddleware) + r.Get("/", app.findIssues) + r.Post("/", app.createIssues) + r.Post("/positions", app.updatePositions) + + r.Route("/{id}", func(r chi.Router) { + r.Get("/", app.findIssuesByID) + r.Delete("/", app.deleteIssues) + }) + }) + + r.Route("/preferences", func(r chi.Router) { + r.Use(app.AuthMiddleware) + r.Get("/", app.getSettings) + }) + + r.Route("/profile", func(r chi.Router) { + r.Use(app.AuthMiddleware) + r.Get("/", app.getProfile) + r.Patch("/", app.updateProfile) + r.Delete("/", app.deleteProfile) + + }) + + r.Route("/users", func(r chi.Router) { + r.Use(app.AuthMiddleware) + r.Get("/", app.findUsers) + }) + }) + + r.Get("/*", func(w http.ResponseWriter, r *http.Request) { + dist, err := fs.Sub(web.DistDir, "dist") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + file, err := dist.Open(strings.TrimPrefix(r.URL.Path, "/")) + if err != nil { + if os.IsNotExist(err) { + r.URL.Path = "/" + } + } else { + defer func(file fs.File) { + err := file.Close() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }(file) + } + + http.FileServer(http.FS(dist)).ServeHTTP(w, r) + }) + + return r +} + +func (app *Application) Start(mux http.Handler) error { + srv := &http.Server{ + Addr: "0.0.0.0:3000", + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + return srv.ListenAndServe() +} diff --git a/internal/server/status.go b/internal/server/status.go new file mode 100644 index 0000000..553d182 --- /dev/null +++ b/internal/server/status.go @@ -0,0 +1,78 @@ +package server + +import ( + "fmt" + "github.com/goccy/go-json" + "madsky.ru/go-tracker/internal/model/status" + "madsky.ru/go-tracker/internal/model/user" + "madsky.ru/go-tracker/internal/server/request" + "madsky.ru/go-tracker/internal/server/response" + "net/http" +) + +func (app *Application) findStatuses(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u := ctx.Value(UserContext{}).(*user.User) + fmt.Println(u) + + s, err := app.Storage.Status.Find(app.Ctx) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, &s) +} + +func (app *Application) FindStatusById(w http.ResponseWriter, r *http.Request) { + id, err := request.Param(r, "id") + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + s, err := app.Storage.Status.FindOne(app.Ctx, id) + if err != nil { + response.Error(w, err, http.StatusNotFound) + return + } + + response.WriteJSON(w, nil, http.StatusOK, &s) +} + +func (app *Application) CreateStatus(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + var statusDto status.CreateStatusDTO + + err := dec.Decode(&statusDto) + if err != nil { + response.Error(w, err, http.StatusNotAcceptable) + return + } + + s, err := app.Storage.Status.Create(app.Ctx, &statusDto) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, &s) +} + +func (app *Application) DeleteStatus(w http.ResponseWriter, r *http.Request) { + id, err := request.Param(r, "id") + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + count, err := app.Storage.Status.Remove(app.Ctx, id) + if err != nil { + response.Error(w, err, http.StatusBadRequest) + return + } + + response.WriteJSON(w, nil, http.StatusOK, count) +} diff --git a/internal/server/user.go b/internal/server/user.go new file mode 100644 index 0000000..b56c6b2 --- /dev/null +++ b/internal/server/user.go @@ -0,0 +1,7 @@ +package server + +import "net/http" + +func (app *Application) findUsers(w http.ResponseWriter, r *http.Request) { + return +} diff --git a/internal/storage/issues.go b/internal/storage/issues.go new file mode 100644 index 0000000..1dd93a2 --- /dev/null +++ b/internal/storage/issues.go @@ -0,0 +1,202 @@ +package storage + +import ( + "context" + "errors" + "fmt" + "log" + "madsky.ru/go-tracker/internal/database" + "madsky.ru/go-tracker/internal/model/issue" + "madsky.ru/go-tracker/internal/model/project" + "madsky.ru/go-tracker/internal/model/status" +) + +type IssueRepository interface { + Find(ctx context.Context) ([]*issue.Issue, error) + UpdatePositions(ctx context.Context, position int, statusId uint32, id uint32) (*uint32, error) + FindOne(ctx context.Context, id uint64) (*issue.Issue, error) + Create(ctx context.Context, dto *issue.CreateIssueDTO) (*uint64, error) + Update(ctx context.Context, id uint64, issue *issue.Issue) error + Remove(ctx context.Context, id uint64) (uint64, error) +} + +type IssueStore struct { + client database.Client +} + +func (is *IssueStore) Find(ctx context.Context) ([]*issue.Issue, error) { + q := `select + i."id", + i."name", + i."description", + i."position", + i."created", + i."project_id", + i."status_id", + p."id" as project_id, + p."name" as project_name, + p."description" as project_description, + p."key" as project_key, + s."id" as status_id, + s."name" as status_name, + s."description" as status_description, + s."position" as position + from issues i + join projects p on p.id = project_id + join statuses s on s.id = status_id` + + rows, err := is.client.Query(ctx, q) + if err != nil { + log.Println("rows", err) + return nil, err + } + + issues := make([]*issue.Issue, 0) + + for rows.Next() { + var n issue.Issue + var p project.Project + var s status.Status + + err = rows.Scan( + &n.ID, + &n.Name, + &n.Description, + &n.Position, + &n.Created, + &n.ProjectID, + &n.StatusID, + &p.ID, + &p.Name, + &p.Description, + &p.Key, + &s.ID, + &s.Name, + &s.Description, + &s.Position, + ) + if err != nil { + log.Println("scan", err) + return nil, err + } + n.Project = p + n.Status = s + issues = append(issues, &n) + } + + if err = rows.Err(); err != nil { + log.Println("rows err", err) + return nil, err + } + + return issues, nil +} + +func (is *IssueStore) UpdatePositions(ctx context.Context, position int, statusId uint32, id uint32) (*uint32, error) { + q := `update issues set "position" = $1, "status_id"=$2 where "id"=$3 returning id` + + var resultId uint32 + + if err := is.client.QueryRow(ctx, q, position, statusId, id).Scan(&resultId); err != nil { + fmt.Println(fmt.Sprintf("error %v", err)) + return nil, err + } + + return &resultId, nil +} + +func (is *IssueStore) FindOne(ctx context.Context, id uint64) (*issue.Issue, error) { + q := `select + i."id", + i."name", + i."description", + i."position", + i."created", + i."project_id", + i."status_id", + p."id" as project_id, + p."name" as project_name, + p."description" as project_description, + p."key" as project_key, + s."id" as status_id, + s."name" as status_name, + s."description" as status_description, + s."position" as position + from issues i + join projects p on p.id = project_id + join statuses s on s.id = status_id + where i."id" = $1` + + var n issue.Issue + var p project.Project + var s status.Status + + if err := is.client.QueryRow(ctx, q, id).Scan( + &n.ID, + &n.Name, + &n.Description, + &n.Position, + &n.Created, + &n.ProjectID, + &n.StatusID, + &p.ID, + &p.Name, + &p.Description, + &p.Key, + &s.ID, + &s.Name, + &s.Description, + &s.Position, + ); err != nil { + fmt.Println(err) + return &issue.Issue{}, err + } + + n.Project = p + n.Status = s + + return &n, nil +} + +func (is *IssueStore) Create(ctx context.Context, dto *issue.CreateIssueDTO) (*uint64, error) { + q := `insert into issues (name, description, project_id, status_id, position) + values ($1, $2, $3, $4, $5) + returning id` + + var position uint32 = 0 + var id uint64 + + if dto.Position != nil { + position = *dto.Position + } + + if err := is.client.QueryRow(ctx, q, dto.Name, dto.Description, dto.ProjectID, dto.StatusID, position).Scan(&id); err != nil { + fmt.Println(fmt.Sprintf("error %v", err)) + return nil, err + } + return &id, nil +} + +func (is *IssueStore) Update(ctx context.Context, id uint64, issue *issue.Issue) error { + //TODO implement me + fmt.Println("update", id, issue, ctx) + panic("implement me") +} + +func (is *IssueStore) Remove(ctx context.Context, id uint64) (uint64, error) { + q := "delete from issues where id=$1" + + tag, err := is.client.Exec(ctx, q, id) + if err != nil { + log.Println("exec error", err) + return 0, err + } + + rowsAffected := tag.RowsAffected() + + if rowsAffected == 0 { + return 0, errors.New("project not found") + } + + return uint64(rowsAffected), nil +} diff --git a/internal/storage/projects.go b/internal/storage/projects.go new file mode 100644 index 0000000..1e89a9a --- /dev/null +++ b/internal/storage/projects.go @@ -0,0 +1,169 @@ +package storage + +import ( + "context" + "errors" + "fmt" + "github.com/jackc/pgx/v5" + "log" + "madsky.ru/go-tracker/internal/database" + "madsky.ru/go-tracker/internal/model/project" + "strings" +) + +type ProjectRepository interface { + Find(ctx context.Context, userId uint32, isAdmin bool, filter *project.FilterDTO) ([]*project.Project, error) + FindOne(ctx context.Context, projectId uint64, userId uint32, isAdmin bool) (*project.Project, error) + Create(ctx context.Context, dto *project.CreateProjectDTO) (*project.Project, error) + Update(ctx context.Context, id uint64, issue *project.UpdateProjectDTO) (*project.Project, error) + Remove(ctx context.Context, id uint64) (uint64, error) +} +type ProjectStore struct { + client database.Client +} + +func (ps *ProjectStore) Find(ctx context.Context, userId uint32, isAdmin bool, filter *project.FilterDTO) ([]*project.Project, error) { + query := `select p.id, p.name, p.description, p.key from projects p ` + + var args []interface{} + + if isAdmin == false { + query += `left join user_to_project up on up.project_id = p.id where up.user_id = $1` + args = append(args, userId) + } + + rows, err := ps.client.Query(ctx, query, args...) + if err != nil { + log.Println("ProjectStore rows", err) + return nil, err + } + defer rows.Close() + + // another variant + //projects, err := pgx.CollectRows(rows, pgx.RowToStructByName[project.Project]) + + projects := make([]*project.Project, 0) + + for rows.Next() { + var p project.Project + + err = rows.Scan(&p.ID, &p.Name, &p.Description, &p.Key) + if err != nil { + log.Println("scan", err) + return nil, err + } + projects = append(projects, &p) + } + + if err = rows.Err(); err != nil { + log.Println("rows err", err) + return nil, err + } + + return projects, nil +} + +func (ps *ProjectStore) FindOne(ctx context.Context, projectId uint64, userId uint32, isAdmin bool) (*project.Project, error) { + args := []interface{}{projectId} + + query := `select p.id, p.name, p.description, p.key from projects p + left join user_to_project up on up.project_id = p.id + where p.id = $1 ` + + if isAdmin == false { + query += "and up.user_id = $2" + args = append(args, userId) + } + + var p project.Project + + rows, _ := ps.client.Query(ctx, query, args...) + defer rows.Close() + + p, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[project.Project]) + if err != nil { + fmt.Println("CollectOneRow FindById project", err) + return nil, err + } + + return &p, nil +} + +func (ps *ProjectStore) Create(ctx context.Context, dto *project.CreateProjectDTO) (*project.Project, error) { + //q := "insert into projects (name, description, key) values ($1, $2, $3) returning id, name, description, key" + query := `insert into projects (name, description, key) + values (@projName, @projDescription, @projKey) + returning id, name, description, key` + + key := dto.Key + + if dto.Key == "" { + key = trimString(dto.Name) + } + + args := pgx.NamedArgs{ + "projName": dto.Name, + "projDescription": dto.Description, + "projKey": key, + } + + //rows, _ := ps.client.Query(ctx, q, dto.Name, dto.Description, key) + rows, _ := ps.client.Query(ctx, query, args) + defer rows.Close() + + p, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[project.Project]) + if err != nil { + fmt.Println("CollectOneRow Create Project", err) + //return nil, fmt.Errorf("unable to insert row: %w", err) + return nil, fmt.Errorf("KEY must be 4 characters long") + } + + return &p, nil +} + +func (ps *ProjectStore) Update(ctx context.Context, id uint64, dto *project.UpdateProjectDTO) (*project.Project, error) { + q := `update projects + set name = $1, description = $2 + where id = $3 + returning id, name, description, key` + + var p project.Project + + if err := ps.client.QueryRow(ctx, q, dto.Name, dto.Description, id).Scan(&p.ID, &p.Name, &p.Description, &p.Key); err != nil { + fmt.Println(fmt.Sprintf("error %v", err)) + return nil, err + } + + return &p, nil +} + +func (ps *ProjectStore) Remove(ctx context.Context, id uint64) (uint64, error) { + q := `delete from projects where id=$1` + + tag, err := ps.client.Exec(ctx, q, id) + if err != nil { + log.Println("exec error", err) + return 0, err + } + + rowsAffected := tag.RowsAffected() + + if rowsAffected == 0 { + return 0, errors.New("project not found") + } + + return uint64(rowsAffected), nil +} + +func trimString(str string) string { + runeStr := []rune(str) + var res string + + if len(runeStr) > 4 { + res = string(runeStr[:3]) + } else { + res = string(runeStr) + } + + return strings.ToUpper(res) +} diff --git a/internal/storage/server-settings.go b/internal/storage/server-settings.go new file mode 100644 index 0000000..2773b36 --- /dev/null +++ b/internal/storage/server-settings.go @@ -0,0 +1,46 @@ +package storage + +import ( + "context" + "log" + "madsky.ru/go-tracker/internal/database" + "madsky.ru/go-tracker/internal/model/setting" +) + +type ServerSettingsRepository interface { + Find(ctx context.Context) ([]*setting.Setting, error) +} + +type ServerSettingsStore struct { + client database.Client +} + +func (store *ServerSettingsStore) Find(ctx context.Context) ([]*setting.Setting, error) { + query := "select id, name, value from public.server_settings" + + rows, err := store.client.Query(ctx, query) + if err != nil { + log.Println("ServerSettingsStore query err", err) + return nil, err + } + + res := make([]*setting.Setting, 0) + + for rows.Next() { + var r setting.Setting + + err := rows.Scan(&r.ID, &r.Name, &r.Value) + if err != nil { + log.Println("ServerSettingsStore scan err", err) + return nil, err + } + res = append(res, &r) + } + + if err := rows.Err(); err != nil { + log.Println("ServerSettingsStore rows err", err) + return nil, err + } + + return res, nil +} diff --git a/internal/storage/status.go b/internal/storage/status.go new file mode 100644 index 0000000..b4892d8 --- /dev/null +++ b/internal/storage/status.go @@ -0,0 +1,99 @@ +package storage + +import ( + "context" + "errors" + "fmt" + "log" + "madsky.ru/go-tracker/internal/database" + "madsky.ru/go-tracker/internal/model/status" +) + +type StatusRepository interface { + Find(ctx context.Context) ([]*status.Status, error) + FindOne(ctx context.Context, id uint64) (*status.Status, error) + Create(ctx context.Context, dto *status.CreateStatusDTO) (*status.Status, error) + Update(ctx context.Context, id uint64, issue *status.Status) error + Remove(ctx context.Context, id uint64) (uint64, error) +} + +type StatusStore struct { + client database.Client +} + +func (r *StatusStore) Find(ctx context.Context) ([]*status.Status, error) { + query := "select id, name, description, position from statuses" + + rows, err := r.client.Query(ctx, query) + if err != nil { + log.Println("rows", err) + return nil, err + } + + statuses := make([]*status.Status, 0) + + for rows.Next() { + var n status.Status + + err = rows.Scan(&n.ID, &n.Name, &n.Description, &n.Position) + if err != nil { + log.Println("scan", err) + return nil, err + } + statuses = append(statuses, &n) + } + + if err = rows.Err(); err != nil { + log.Println("rows err", err) + return nil, err + } + + return statuses, nil +} + +func (r *StatusStore) FindOne(ctx context.Context, id uint64) (*status.Status, error) { + query := "select id, name, description, position from statuses where id = $1" + + var s status.Status + if err := r.client.QueryRow(ctx, query, id).Scan(&s.ID, &s.Name, &s.Description, &s.Position); err != nil { + fmt.Println(err) + return nil, err + } + + return &s, nil +} + +func (r *StatusStore) Create(ctx context.Context, dto *status.CreateStatusDTO) (*status.Status, error) { + q := "insert into statuses (name, description) values ($1, $2) returning id, name, description, position" + + var s status.Status + + if err := r.client.QueryRow(ctx, q, dto.Name, dto.Description).Scan(&s.ID, &s.Name, &s.Description, &s.Position); err != nil { + fmt.Println(fmt.Sprintf("error %v", err)) + return nil, err + } + return &s, nil +} + +func (r *StatusStore) Update(ctx context.Context, id uint64, issue *status.Status) error { + //TODO implement me + panic("implement me") +} + +func (r *StatusStore) Remove(ctx context.Context, id uint64) (uint64, error) { + q := "delete from statuses where id=$1" + + tag, err := r.client.Exec(ctx, q, id) + if err != nil { + log.Println("exec error", err) + return 0, err + } + + rowsAffected := tag.RowsAffected() + + if rowsAffected == 0 { + return 0, errors.New("status not found") + } + + return uint64(rowsAffected), nil +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..1031ec5 --- /dev/null +++ b/internal/storage/storage.go @@ -0,0 +1,25 @@ +package storage + +import ( + "github.com/jackc/pgx/v5/pgxpool" +) + +type Storage struct { + Projects ProjectRepository + ServerSettings ServerSettingsRepository + Issues IssueRepository + Status StatusRepository + User UserRepository + UserToProject UserToProjectRepository +} + +func NewStorage(client *pgxpool.Pool) *Storage { + return &Storage{ + Projects: &ProjectStore{client: client}, + ServerSettings: &ServerSettingsStore{client: client}, + Issues: &IssueStore{client: client}, + Status: &StatusStore{client: client}, + User: &UserStore{client: client}, + UserToProject: &UserToProjectStore{client: client}, + } +} diff --git a/internal/storage/user-to-project.go b/internal/storage/user-to-project.go new file mode 100644 index 0000000..c016860 --- /dev/null +++ b/internal/storage/user-to-project.go @@ -0,0 +1,32 @@ +package storage + +import ( + "context" + "fmt" + "github.com/jackc/pgx/v5" + "madsky.ru/go-tracker/internal/database" +) + +type UserToProjectRepository interface { + Create(ctx context.Context, userId uint32, projectId uint32) error +} + +type UserToProjectStore struct { + client database.Client +} + +func (up *UserToProjectStore) Create(ctx context.Context, userId uint32, projectId uint32) error { + query := `insert into user_to_project (user_id, project_id) values (@userId, @projectId)` + + args := pgx.NamedArgs{ + "userId": userId, + "projectId": projectId, + } + + _, err := up.client.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("unable to insert row: %w", err) + } + + return nil +} diff --git a/internal/storage/user.go b/internal/storage/user.go new file mode 100644 index 0000000..33e9f1d --- /dev/null +++ b/internal/storage/user.go @@ -0,0 +1,108 @@ +package storage + +import ( + "context" + "fmt" + "github.com/jackc/pgx/v5" + "madsky.ru/go-tracker/internal/database" + "madsky.ru/go-tracker/internal/model/user" +) + +type UserRepository interface { + FindByEmail(ctx context.Context, email string) (*user.User, error) + FindById(ctx context.Context, id uint32) (*user.User, error) + Create(ctx context.Context, user *user.User) (*user.User, error) + IsEmpty(ctx context.Context) (bool, error) + Update(ctx context.Context, id uint32, dto *user.UpdateUserDTO) (*user.User, error) + //Find(ctx context.Context) ([]*user.User, error) + //FindOne(ctx context.Context, id uint64) (*user.User, error) + //Create(ctx context.Context, dto *user.CreateUserDTO) (*user.User, error) + //Remove(ctx context.Context, id uint64) (uint64, error) +} + +type UserStore struct { + client database.Client +} + +func (us *UserStore) FindById(ctx context.Context, id uint32) (*user.User, error) { + query := `select id, email, name, password_hash, role + from users + where id = $1` + + rows, _ := us.client.Query(ctx, query, id) + defer rows.Close() + + u, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[user.User]) + if err != nil { + fmt.Println("CollectOneRow FindById User", err) + return nil, err + } + + return &u, nil +} + +func (us *UserStore) FindByEmail(ctx context.Context, email string) (*user.User, error) { + query := `select id, email, name, password_hash, role + from users + where email = $1` + + rows, _ := us.client.Query(ctx, query, email) + defer rows.Close() + + u, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[user.User]) + if err != nil { + fmt.Println("CollectOneRow FindByEmail User", err) + return nil, err + } + + return &u, nil +} + +func (us *UserStore) IsEmpty(ctx context.Context) (bool, error) { + query := "select count(id) from users limit 1" + + rows, _ := us.client.Query(ctx, query) + defer rows.Close() + + count, err := pgx.CollectOneRow(rows, pgx.RowTo[int]) + if err != nil { + fmt.Println("CollectOneRow IsEmpty User", err) + return false, err + } + + return count == 0, nil +} + +func (us *UserStore) Create(ctx context.Context, dto *user.User) (*user.User, error) { + query := `insert into users (email, name, password_hash, role) + values ($1, $2, $3, $4) + returning id, name, email, password_hash, role` + rows, _ := us.client.Query(ctx, query, dto.Email, dto.Name, dto.PasswordHash, dto.Role) + defer rows.Close() + + u, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[user.User]) + if err != nil { + fmt.Println("CollectOneRow Create User", err) + return nil, err + } + + return &u, nil +} + +func (us *UserStore) Update(ctx context.Context, id uint32, dto *user.UpdateUserDTO) (*user.User, error) { + query := `update users + set name = $1 + where id = $2 + returning id, email, name, password_hash, role` + + rows, _ := us.client.Query(ctx, query, dto.Name, id) + defer rows.Close() + + u, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[user.User]) + if err != nil { + fmt.Println("CollectOneRow Update User", err) + return nil, err + } + + return &u, nil +} diff --git a/proto/hello.pb.go b/proto/hello.pb.go new file mode 100644 index 0000000..ffb4c0b --- /dev/null +++ b/proto/hello.pb.go @@ -0,0 +1,180 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v6.30.0--rc2 +// source: proto/hello.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type HelloRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HelloRequest) Reset() { + *x = HelloRequest{} + mi := &file_proto_hello_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloRequest) ProtoMessage() {} + +func (x *HelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_hello_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. +func (*HelloRequest) Descriptor() ([]byte, []int) { + return file_proto_hello_proto_rawDescGZIP(), []int{0} +} + +func (x *HelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type HelloResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HelloResponse) Reset() { + *x = HelloResponse{} + mi := &file_proto_hello_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HelloResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloResponse) ProtoMessage() {} + +func (x *HelloResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_hello_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloResponse.ProtoReflect.Descriptor instead. +func (*HelloResponse) Descriptor() ([]byte, []int) { + return file_proto_hello_proto_rawDescGZIP(), []int{1} +} + +func (x *HelloResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_proto_hello_proto protoreflect.FileDescriptor + +var file_proto_hello_proto_rawDesc = string([]byte{ + 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, + 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x29, + 0x0a, 0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x40, 0x0a, 0x05, 0x48, 0x65, 0x6c, + 0x6c, 0x6f, 0x12, 0x37, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x13, + 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x1c, 0x5a, 0x1a, 0x6d, + 0x61, 0x64, 0x73, 0x6b, 0x79, 0x2e, 0x72, 0x75, 0x2f, 0x67, 0x6f, 0x2d, 0x66, 0x69, 0x6e, 0x61, + 0x6e, 0x63, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +}) + +var ( + file_proto_hello_proto_rawDescOnce sync.Once + file_proto_hello_proto_rawDescData []byte +) + +func file_proto_hello_proto_rawDescGZIP() []byte { + file_proto_hello_proto_rawDescOnce.Do(func() { + file_proto_hello_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_hello_proto_rawDesc), len(file_proto_hello_proto_rawDesc))) + }) + return file_proto_hello_proto_rawDescData +} + +var file_proto_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proto_hello_proto_goTypes = []any{ + (*HelloRequest)(nil), // 0: hello.HelloRequest + (*HelloResponse)(nil), // 1: hello.HelloResponse +} +var file_proto_hello_proto_depIdxs = []int32{ + 0, // 0: hello.Hello.SayHello:input_type -> hello.HelloRequest + 1, // 1: hello.Hello.SayHello:output_type -> hello.HelloResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proto_hello_proto_init() } +func file_proto_hello_proto_init() { + if File_proto_hello_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_hello_proto_rawDesc), len(file_proto_hello_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_hello_proto_goTypes, + DependencyIndexes: file_proto_hello_proto_depIdxs, + MessageInfos: file_proto_hello_proto_msgTypes, + }.Build() + File_proto_hello_proto = out.File + file_proto_hello_proto_goTypes = nil + file_proto_hello_proto_depIdxs = nil +} diff --git a/proto/hello.proto b/proto/hello.proto new file mode 100644 index 0000000..69ed9c7 --- /dev/null +++ b/proto/hello.proto @@ -0,0 +1,17 @@ +syntax="proto3"; + +package hello; + +option go_package = "madsky.ru/go-tracker/proto"; + +service Hello { + rpc SayHello (HelloRequest) returns (HelloResponse) {}; +} + +message HelloRequest { + string name = 1; +} + +message HelloResponse { + string message = 1; +} \ No newline at end of file diff --git a/proto/hello_grpc.pb.go b/proto/hello_grpc.pb.go new file mode 100644 index 0000000..39d8305 --- /dev/null +++ b/proto/hello_grpc.pb.go @@ -0,0 +1,121 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.30.0--rc2 +// source: proto/hello.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Hello_SayHello_FullMethodName = "/hello.Hello/SayHello" +) + +// HelloClient is the client API for Hello service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type HelloClient interface { + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) +} + +type helloClient struct { + cc grpc.ClientConnInterface +} + +func NewHelloClient(cc grpc.ClientConnInterface) HelloClient { + return &helloClient{cc} +} + +func (c *helloClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HelloResponse) + err := c.cc.Invoke(ctx, Hello_SayHello_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// HelloServer is the server API for Hello service. +// All implementations must embed UnimplementedHelloServer +// for forward compatibility. +type HelloServer interface { + SayHello(context.Context, *HelloRequest) (*HelloResponse, error) + mustEmbedUnimplementedHelloServer() +} + +// UnimplementedHelloServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedHelloServer struct{} + +func (UnimplementedHelloServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") +} +func (UnimplementedHelloServer) mustEmbedUnimplementedHelloServer() {} +func (UnimplementedHelloServer) testEmbeddedByValue() {} + +// UnsafeHelloServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HelloServer will +// result in compilation errors. +type UnsafeHelloServer interface { + mustEmbedUnimplementedHelloServer() +} + +func RegisterHelloServer(s grpc.ServiceRegistrar, srv HelloServer) { + // If the following call pancis, it indicates UnimplementedHelloServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Hello_ServiceDesc, srv) +} + +func _Hello_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HelloServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Hello_SayHello_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HelloServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Hello_ServiceDesc is the grpc.ServiceDesc for Hello service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Hello_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "hello.Hello", + HandlerType: (*HelloServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _Hello_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/hello.proto", +} diff --git a/sql/down.sql b/sql/down.sql new file mode 100644 index 0000000..e663cef --- /dev/null +++ b/sql/down.sql @@ -0,0 +1,8 @@ +drop table server_settings; +drop table user_to_issue; +drop table user_to_project; +drop table categories; +drop table issues; +drop table users; +drop table projects; +drop table statuses; \ No newline at end of file diff --git a/sql/up.sql b/sql/up.sql new file mode 100644 index 0000000..751cff9 --- /dev/null +++ b/sql/up.sql @@ -0,0 +1,100 @@ +-- settings +create table if not exists server_settings +( + id integer primary key generated by default as identity, + name varchar not null, + value varchar not null +); +insert into server_settings (name, value) +values ('version', '1.0.0'); + +-- projects +create table if not exists projects +( + id integer primary key generated by default as identity, + name varchar not null, + description varchar, + key varchar(4) not null +); +insert into projects (name, key) +values ('demo', 'DEMO'),('madsky', 'MAD'); + +-- statuses +create table if not exists statuses +( + id integer primary key generated by default as identity, + name varchar not null, + description varchar, + position integer default 0 not null +); + +insert into statuses (name) +values ('todo'), + ('is working'), + ('done'); + +-- issues +create table if not exists issues +( + id integer primary key generated by default as identity, + name varchar not null, + description varchar, + position integer default 0 not null, + created timestamptz DEFAULT now() not null, + status_id integer not null, + project_id integer not null, + foreign key (status_id) references statuses, + foreign key (project_id) references projects +); + +-- users +create table if not exists users +( + id integer primary key generated by default as identity, + email varchar not null unique, + password_hash varchar, + name varchar, + avatar varchar, + role varchar default 'user' +); +create index if not exists user_email_index on users (email); +-- insert into users (email, password_hash, name, isAdmin) +-- values ('admin@admin.ru', '', 'admin', true); + +-- relation +create table if not exists user_to_issue +( + user_id integer, + issue_id integer, + foreign key (user_id) references users, + foreign key (issue_id) references issues on delete cascade, + constraint user_to_issue_primary_key primary key (user_id, issue_id) +); +create index if not exists user_to_issue_user_id_index on user_to_issue (user_id); +create index if not exists user_to_issue_issue_id_index on user_to_issue (issue_id); + +create table if not exists user_to_project +( + user_id integer, + project_id integer, + foreign key (user_id) references users, + foreign key (project_id) references projects on delete cascade, + constraint user_to_project_primary_key primary key (user_id, project_id) +); +create index if not exists user_to_project_user_id_index on user_to_project (user_id); +create index if not exists user_to_project_project_id_index on user_to_project (project_id); + + +-- categories +create table if not exists categories +( + id integer primary key generated by default as identity, + name varchar not null, + description varchar, + color varchar(7) default '#000000', + image varchar, + user_id integer not null, + foreign key (user_id) references users +); +-- insert into categories (name, color, user_id) +-- values ('Default', '#ffffff', 1);