This commit is contained in:
2026-03-06 17:45:02 +03:00
commit d6e833bc9f
57 changed files with 2682 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
/bin/
/config/config.yaml

20
Makefile Normal file
View File

@@ -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:
CGO_ENABLED=0 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

1
README.md Normal file
View File

@@ -0,0 +1 @@
# Hello world

43
cmd/grpc.go Normal file
View File

@@ -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)
}
}

69
cmd/main.go Normal file
View File

@@ -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)
}

View File

@@ -0,0 +1,10 @@
app_name: 'APP'
nats:
host: ''
token: ''
database:
host: ''
port: 5432
user: ''
password: ''
name: 'tracker'

41
go.mod Normal file
View File

@@ -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
)

108
go.sum Normal file
View File

@@ -0,0 +1,108 @@
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.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.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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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.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-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=

23
internal/broker/broker.go Normal file
View File

@@ -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
}

42
internal/config/config.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"

10
internal/logger/logger.go Normal file
View File

@@ -0,0 +1,10 @@
package logger
import (
"log/slog"
"os"
)
func NewLogger() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stdout, nil))
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -0,0 +1,7 @@
package setting
type Setting struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Value string `json:"value"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

150
internal/server/auth.go Normal file
View File

@@ -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(&registerDto); 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)
}

View File

@@ -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)
}

View File

@@ -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
}

104
internal/server/issue.go Normal file
View File

@@ -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)
}

View File

@@ -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)
})
}

View File

@@ -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)
}

147
internal/server/project.go Normal file
View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

157
internal/server/server.go Normal file
View File

@@ -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()
}

78
internal/server/status.go Normal file
View File

@@ -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)
}

7
internal/server/user.go Normal file
View File

@@ -0,0 +1,7 @@
package server
import "net/http"
func (app *Application) findUsers(w http.ResponseWriter, r *http.Request) {
return
}

202
internal/storage/issues.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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},
}
}

View File

@@ -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
}

108
internal/storage/user.go Normal file
View File

@@ -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
}

180
proto/hello.pb.go Normal file
View File

@@ -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
}

17
proto/hello.proto Normal file
View File

@@ -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;
}

121
proto/hello_grpc.pb.go Normal file
View File

@@ -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",
}

8
sql/down.sql Normal file
View File

@@ -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;

100
sql/up.sql Normal file
View File

@@ -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);

View File

@@ -0,0 +1 @@
import{d as f,h as s,w as n,b as e,r as o,c as l,F as r,A as b,p as _,o as c,e as g,t as h}from"./index-KpQc9ZlS.js";const D=f({__name:"DynamicMenu",props:{menu:{},itemId:{},isHovering:{type:[Boolean,null]}},setup(m){const i=m;return(B,C)=>{const a=o("v-icon"),d=o("v-btn"),p=o("v-divider"),v=o("v-list-item-title"),u=o("v-list-item"),k=o("v-list"),y=o("v-menu");return c(),s(y,_(i,{width:"200"}),{activator:n(({props:t})=>[e(d,_({icon:"",rounded:"",variant:i.isHovering?"tonal":"text"},t),{default:n(()=>[e(a,{icon:"mdi-dots-horizontal"})]),_:2},1040,["variant"])]),default:n(()=>[e(k,{density:"compact",class:"mt-1"},{default:n(()=>[(c(!0),l(r,null,b(i.menu,t=>(c(),l(r,{key:t.id},[t.type==="divider"?(c(),s(p,{key:0,class:"my-1"})):(c(),s(u,{key:1,disabled:t.disabled,color:t.color,onClick:x=>t.click&&t.click(i.itemId)},{default:n(()=>[e(v,null,{default:n(()=>[e(a,{class:"mr-2",color:t.color,icon:t.icon},null,8,["color","icon"]),g("span",null,h(t.title),1)]),_:2},1024)]),_:2},1032,["disabled","color","onClick"]))],64))),128))]),_:1})]),_:1},16)}}});export{D as _};

View File

@@ -0,0 +1 @@
.sortable-ghost[data-v-1c47e356]{opacity:.33;transition:opacity .3s}.sortable-drag[data-v-1c47e356]{background-color:rgb(var(--v-theme-background))}.v-tabs .v-tabs-slider-wrapper[data-v-93eecf16]{transition:none}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5
web/dist/assets/index-Bsusvx-X.css vendored Normal file

File diff suppressed because one or more lines are too long

58
web/dist/assets/index-KpQc9ZlS.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
web/dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

15
web/dist/index.html vendored Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="mobile-web-app-capable" content="yes">
<title>Vite App</title>
<script type="module" crossorigin src="/assets/index-KpQc9ZlS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bsusvx-X.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

8
web/embed.go Normal file
View File

@@ -0,0 +1,8 @@
package web
import (
"embed"
)
//go:embed all:dist
var DistDir embed.FS