From 2fa110e760655d021d3f956d5733261bba7995b9 Mon Sep 17 00:00:00 2001 From: new_gate <> Date: Sat, 27 Jul 2024 07:31:04 +0000 Subject: [PATCH] Initial commit --- Dockerfile | 9 + Makefile | 4 + buf.gen.yaml | 12 + buf.yaml | 7 + docker-compose.yaml | 75 +++++ go.mod | 37 +++ go.sum | 80 +++++ internal/app/app.go | 35 +++ internal/lib/config.go | 27 ++ internal/lib/errors.go | 22 ++ internal/lib/lib.go | 40 +++ internal/lib/mail.go | 19 ++ internal/lib/validation.go | 44 +++ internal/services/email.go | 1 + internal/services/session.go | 147 +++++++++ internal/services/user.go | 244 +++++++++++++++ internal/storage/postgresql.go | 267 +++++++++++++++++ internal/storage/valkey.go | 332 +++++++++++++++++++++ internal/transport/email_server.go | 11 + internal/transport/interceptors.go | 1 + internal/transport/server.go | 123 ++++++++ internal/transport/session_server.go | 45 +++ internal/transport/user_server.go | 124 ++++++++ main.go | 47 +++ migrations/20240608163806_initial.sql | 43 +++ pkg/go/gen/user/v1/user.pb.go | 412 ++++++++++++++++++++++++++ pkg/go/gen/user/v1/user_grpc.pb.go | 105 +++++++ proto/user/v1/user.proto | 33 +++ 28 files changed, 2346 insertions(+) create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 docker-compose.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/app/app.go create mode 100644 internal/lib/config.go create mode 100644 internal/lib/errors.go create mode 100644 internal/lib/lib.go create mode 100644 internal/lib/mail.go create mode 100644 internal/lib/validation.go create mode 100644 internal/services/email.go create mode 100644 internal/services/session.go create mode 100644 internal/services/user.go create mode 100644 internal/storage/postgresql.go create mode 100644 internal/storage/valkey.go create mode 100644 internal/transport/email_server.go create mode 100644 internal/transport/interceptors.go create mode 100644 internal/transport/server.go create mode 100644 internal/transport/session_server.go create mode 100644 internal/transport/user_server.go create mode 100644 main.go create mode 100644 migrations/20240608163806_initial.sql create mode 100644 pkg/go/gen/user/v1/user.pb.go create mode 100644 pkg/go/gen/user/v1/user_grpc.pb.go create mode 100644 proto/user/v1/user.proto diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..78e7a26 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:latest + +WORKDIR /app + +COPY . . + +RUN go build ./main.go + +CMD ["./main"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..abb1ae8 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +all: + go build + bug generate proto + diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..d5b53a4 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,12 @@ +version: v1 +managed: + enabled: true + go_package_prefix: + default: ms-auth/pkg/go/gen +plugins: + - name: go + out: pkg/go/gen + opt: paths=source_relative + - name: go-grpc + out: pkg/go/gen + opt: paths=source_relative diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..1a51945 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,7 @@ +version: v1 +breaking: + use: + - FILE +lint: + use: + - DEFAULT diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..d3a96e9 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,75 @@ +version: '3' + +networks: + local: + +volumes: + db: + +services: + auth-service: + build: + dockerfile: ./Dockerfile + env_file: + - .env + ports: + - "8090:8090" + depends_on: + # postgres: + # condition: service_healthy + # valkey: + # condition: service_healthy + migrate: + condition: service_completed_successfully + networks: + - local + + postgres: + image: postgres:14.1-alpine + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: supersecretpassword + networks: + - local + ports: + - '5432:5432' + volumes: + - db:/var/lib/postgresql/data + healthcheck: + test: pg_isready -U postgres -d postgres + interval: 10s + timeout: 3s + retries: 5 + + migrate: + image: ghcr.io/kukymbr/goose-docker:latest + networks: + - local + volumes: + - ./migrations:/migrations + environment: + GOOSE_DRIVER: "postgres" + GOOSE_DBSTRING: "host=postgres user=postgres password=supersecretpassword dbname=postgres port=5432 sslmode=disable" + depends_on: + postgres: + condition: service_healthy + + valkey: + container_name: valkey + hostname: valkey + image: valkey/valkey:latest + build: . + volumes: + - ./conf/valkey.conf:/usr/local/etc/valkey/valkey.conf + - ./data:/data + command: ["valkey-server", "/usr/local/etc/valkey/valkey.conf"] + healthcheck: + test: ["CMD-SHELL", "valkey-cli ping | grep PONG"] + interval: 1s + timeout: 3s + retries: 5 + ports: + - 6379:6379 + networks: + - local diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..859a740 --- /dev/null +++ b/go.mod @@ -0,0 +1,37 @@ +module ms-auth + +go 1.19 + +require ( + github.com/google/uuid v1.6.0 + github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/valkey-io/valkey-go v1.0.38 + golang.org/x/crypto v0.24.0 + google.golang.org/grpc v1.64.0 + google.golang.org/protobuf v1.34.1 +) + +require ( + github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect +) + +require ( + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/jackc/pgx/v5 v5.6.0 + github.com/jmoiron/sqlx v1.4.0 + github.com/joho/godotenv v1.5.1 // 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..3c1c147 --- /dev/null +++ b/go.sum @@ -0,0 +1,80 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +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/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= +github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/valkey-io/valkey-go v1.0.38 h1:0g+ozx+WUGmu18h8SVGoIyxdWp820utVLlFkGnQ3h0c= +github.com/valkey-io/valkey-go v1.0.38/go.mod h1:LXqAbjygRuA1YRocojTslAGx2dQB4p8feaseGviWka4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +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/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..d97d15e --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,35 @@ +package app + +import ( + "log/slog" + "ms-auth/internal/lib" + + _ "github.com/jackc/pgx/v5/stdlib" +) + +type Server interface { + Start() + GracefullyStop() +} + +type App struct { + server Server + cfg *lib.Config +} + +func NewApp(cfg *lib.Config, server Server) *App { + return &App{ + server: server, + cfg: cfg, + } +} + +func (app *App) Start() { + app.server.Start() + slog.Info("app started") +} + +func (app *App) GracefullyStop() { + app.server.GracefullyStop() + slog.Info("app stopped") +} diff --git a/internal/lib/config.go b/internal/lib/config.go new file mode 100644 index 0000000..443f158 --- /dev/null +++ b/internal/lib/config.go @@ -0,0 +1,27 @@ +package lib + +import ( + "fmt" + "github.com/ilyakaznacheev/cleanenv" +) + +type Config struct { + Env string `env:"ENV" env-default:"prod"` + + PostgresDSN string `env:"POSTGRES_DSN" required:"true"` + RedisDSN string `env:"REDIS_DSN" required:"true"` + + Email string `env:"EMAIL" required:"true"` + Password string `env:"PASSWORD" required:"true"` + + JWTSecret string `env:"JWT_SECRET" required:"true"` +} + +func MustSetupConfig() *Config { + var cfg Config + err := cleanenv.ReadConfig(".env", &cfg) + if err != nil { + panic(fmt.Sprintf("error reading config: %s", err.Error())) + } + return &cfg +} diff --git a/internal/lib/errors.go b/internal/lib/errors.go new file mode 100644 index 0000000..f63a456 --- /dev/null +++ b/internal/lib/errors.go @@ -0,0 +1,22 @@ +package lib + +import ( + "errors" +) + +var ( + ErrInternal = errors.New("internal") + ErrUnexpected = errors.New("unexpected") + ErrNoPermission = errors.New("no permission") +) + +var ( + ErrBadHandleOrPassword = errors.New("bad handle or password") + ErrBadRole = errors.New("bad role") + ErrTooShortPassword = errors.New("too short password") + ErrTooLongPassword = errors.New("too long password") + ErrBadEmail = errors.New("bad email") + ErrBadUsername = errors.New("bad username") + ErrTooShortUsername = errors.New("too short username") + ErrTooLongUsername = errors.New("too long username") +) diff --git a/internal/lib/lib.go b/internal/lib/lib.go new file mode 100644 index 0000000..744d091 --- /dev/null +++ b/internal/lib/lib.go @@ -0,0 +1,40 @@ +package lib + +import ( + "time" +) + +const ( + RoleSpectator int32 = 0 + RoleParticipant int32 = 1 + RoleModerator int32 = 2 + RoleAdmin int32 = 3 +) + +func IsAdmin(role int32) bool { + return role == RoleAdmin +} + +func IsModerator(role int32) bool { + return role == RoleModerator +} + +func IsParticipant(role int32) bool { + return role == RoleParticipant +} + +func IsSpectator(role int32) bool { + return role == RoleSpectator +} + +func AsTimeP(t time.Time) *time.Time { + return &t +} + +func AsInt32P(v int32) *int32 { + return &v +} + +func AsStringP(str string) *string { + return &str +} diff --git a/internal/lib/mail.go b/internal/lib/mail.go new file mode 100644 index 0000000..bac4b82 --- /dev/null +++ b/internal/lib/mail.go @@ -0,0 +1,19 @@ +package lib + +import ( + "fmt" + "net/smtp" +) + +func SendMail(cfg Config, to []string, subject, body string) error { + auth := smtp.PlainAuth("", cfg.Email, cfg.Password, "smtp.gmail.com") + + msg := fmt.Sprintf("From: %s\nTo: %s\nSubject: %s\n%s", cfg.Email, "", subject, body) + + err := smtp.SendMail("smtp.gmail.com:587", auth, cfg.Email, to, []byte(msg)) + if err != nil { + return err // FIXME + } + + return nil +} diff --git a/internal/lib/validation.go b/internal/lib/validation.go new file mode 100644 index 0000000..e1848e3 --- /dev/null +++ b/internal/lib/validation.go @@ -0,0 +1,44 @@ +package lib + +import ( + "net/mail" +) + +func ValidPassword(str string) error { + if len(str) < 5 { + return ErrTooShortPassword + } + if len(str) > 70 { + return ErrTooLongPassword + } + return nil +} + +func ValidUsername(str string) error { + if len(str) < 5 { + return ErrTooShortUsername + } + if len(str) > 70 { + return ErrTooLongUsername + } + if err := ValidEmail(str); err == nil { + return ErrBadUsername + } + return nil +} + +func ValidEmail(str string) error { + emailAddress, err := mail.ParseAddress(str) + if err != nil || emailAddress.Address != str { + return ErrBadEmail + } + return nil +} + +func ValidRole(role int32) error { + switch role { + case RoleSpectator, RoleParticipant, RoleModerator, RoleAdmin: + return nil + } + return ErrBadRole +} diff --git a/internal/services/email.go b/internal/services/email.go new file mode 100644 index 0000000..5e568ea --- /dev/null +++ b/internal/services/email.go @@ -0,0 +1 @@ +package services diff --git a/internal/services/session.go b/internal/services/session.go new file mode 100644 index 0000000..7db7bbe --- /dev/null +++ b/internal/services/session.go @@ -0,0 +1,147 @@ +package services + +import ( + "context" + "ms-auth/internal/lib" + "ms-auth/internal/storage" +) + +type SessionProvider interface { + CreateSession(ctx context.Context, userId int32) error + ReadSessionByToken(ctx context.Context, token string) (*storage.Session, error) + ReadSessionByUserId(ctx context.Context, userId int32) (*storage.Session, error) + UpdateSession(ctx context.Context, session *storage.Session) error + DeleteSessionByToken(ctx context.Context, token string) error + DeleteSessionByUserId(ctx context.Context, userId int32) error +} + +// SessionService represents a service for managing sessions. +type SessionService struct { + sessionProvider SessionProvider + userProvider UserProvider + cfg *lib.Config +} + +// NewSessionService creates a new SessionService instance. +// +// Parameters: +// - sessionProvider: The SessionProvider implementation used by the SessionService. +// - userProvider: The UserProvider implementation used by the SessionService. +// - cfg: The lib.Config object used by the SessionService. +// +// Returns: +// - *SessionService: A pointer to the SessionService instance. +func NewSessionService(sessionProvider SessionProvider, userProvider UserProvider, cfg *lib.Config) *SessionService { + return &SessionService{ + sessionProvider: sessionProvider, + userProvider: userProvider, + cfg: cfg, + } +} + +// Create creates a new session for a user with the given handle and password. +// +// Parameters: +// - ctx: The context.Context object for the request. +// - handle: The handle (username or email) of the user. +// - password: The password of the user. +// +// Returns: +// - *string: A pointer to the token of the newly created session, or nil if there was an error. +// - error: An error if the creation of the session or the retrieval of the session's token failed. +func (s *SessionService) Create(ctx context.Context, handle, password string) (*string, error) { + var ( + err error + user *storage.User + ) + + if lib.ValidUsername(handle) == nil { + user, err = s.userProvider.ReadUserByUsername(ctx, handle) + } else if lib.ValidEmail(handle) == nil { + user, err = s.userProvider.ReadUserByEmail(ctx, handle) + } else { + return nil, lib.ErrBadHandleOrPassword + } + if err != nil { + return nil, err + } + + err = user.ComparePassword(password) + if err != nil { + return nil, err + } + + err = s.sessionProvider.CreateSession(ctx, user.Id) + if err != nil { + return nil, err + } + + session, err := s.sessionProvider.ReadSessionByUserId(ctx, user.Id) + if err != nil { + return nil, err + } + + token, err := session.Token(s.cfg.JWTSecret) + if err != nil { + return nil, err + } + + return &token, nil +} + +// Read retrieves the user ID associated with the given session token. +// +// Parameters: +// - ctx: The context.Context object for the request. +// - token: The session token. +// +// Returns: +// - *int32: The user ID associated with the session token, or nil if an error occurs. +// - error: An error object if any error occurs during the retrieval process. +func (s *SessionService) Read(ctx context.Context, token string) (*int32, error) { + session, err := s.sessionProvider.ReadSessionByToken(ctx, token) + if err != nil { + return nil, err + } + return session.UserId, nil +} + +// Update updates the session associated with the given token. +// +// Parameters: +// - ctx: The context.Context object for the request. +// - token: The session token. +// +// Returns: +// - error: An error object if any error occurs during the update process. +func (s *SessionService) Update(ctx context.Context, token string) error { + session, err := s.sessionProvider.ReadSessionByToken(ctx, token) + if err != nil { + return err + } + err = s.sessionProvider.UpdateSession(ctx, session) + if err != nil { + return err + } + return nil +} + +// Delete deletes the session associated with the given token. +// +// Parameters: +// - ctx: The context.Context object for the request. +// - token: The session token. +// +// Returns: +// - error: An error object if any error occurs during the deletion process. +func (s *SessionService) Delete(ctx context.Context, token string) error { + session, err := s.sessionProvider.ReadSessionByToken(ctx, token) + if err != nil { + return err + } + err = s.sessionProvider.DeleteSessionByUserId(ctx, *session.UserId) + if err != nil { + return err + } + return nil +} diff --git a/internal/services/user.go b/internal/services/user.go new file mode 100644 index 0000000..ee4d8e3 --- /dev/null +++ b/internal/services/user.go @@ -0,0 +1,244 @@ +package services + +import ( + "context" + "ms-auth/internal/lib" + "ms-auth/internal/storage" + "time" +) + +type UserProvider interface { + CreateUser( + ctx context.Context, + username string, + password string, + email *string, + expiresAt *time.Time, + role *int32, + ) (*int32, error) + ReadUserByEmail(ctx context.Context, email string) (*storage.User, error) + ReadUserByUsername(ctx context.Context, username string) (*storage.User, error) + ReadUserById(ctx context.Context, id int32) (*storage.User, error) + UpdateUser( + ctx context.Context, + id int32, + username *string, + password *string, + email *string, + expiresAt *time.Time, + role *int32, + ) error + DeleteUser(ctx context.Context, id int32) error +} + +type ConfirmationProvider interface { + CreateConfirmation(ctx context.Context, conf *storage.Confirmation) error + ReadConfirmation(ctx context.Context, confId string) (*storage.Confirmation, error) + DeleteConfirmation(ctx context.Context, confId string) error +} + +type EmailProvider interface { + SendMail(ctx context.Context, to []string, subject string, body string) error +} + +// UserService represents a service for managing users. +type UserService struct { + userProvider UserProvider + sessionProvider SessionProvider + confirmationProvider ConfirmationProvider + //emailProvider EmailProvider + cfg *lib.Config +} + +// NewUserService creates a new UserService instance. +// +// Parameters: +// - userProvider: The UserProvider implementation used by the UserService. +// - sessionProvider: The SessionProvider implementation used by the UserService. +// - confirmationProvider: The ConfirmationProvider implementation used by the UserService. +// - emailProvider: The EmailProvider implementation used by the UserService. +// - cfg: The lib.Config object used by the UserService. +// +// Returns: +// - *UserService: A pointer to the newly created UserService instance. +func NewUserService( + userProvider UserProvider, + sessionProvider SessionProvider, + confirmationProvider ConfirmationProvider, + //emailProvider EmailProvider, + cfg *lib.Config, +) *UserService { + return &UserService{ + userProvider: userProvider, + sessionProvider: sessionProvider, + confirmationProvider: confirmationProvider, + //emailProvider: emailProvider, + cfg: cfg, + } +} + +// CreateUser creates a new user with the provided information. +// +// Parameters: +// - ctx: The context for the operation. +// - token: The token associated with the session. +// - username: The username of the new user. +// - password: The password of the new user. +// - email: The email of the new user (can be nil). +// - expiresAt: The expiration time for the user account (can be nil). +// - role: The role of the new user. +// +// Returns: +// - *int32: The ID of the created user. +// - error: An error if the operation fails. +func (u *UserService) CreateUser(ctx context.Context, token, username, password string, email *string, expiresAt *time.Time, role *int32) (*int32, error) { + user, err := u.ReadUserBySessionToken(ctx, token) + if err != nil { + return nil, err + } + + canCreate := func() bool { + if !user.IsAdmin() && !user.IsModerator() { + return false + } + + if role != nil && user.IsModerator() { + if lib.IsModerator(*role) || lib.IsAdmin(*role) { + return false + } + } + return true + }() + + if !canCreate { + return nil, lib.ErrNoPermission + } + + return u.userProvider.CreateUser(ctx, username, password, email, expiresAt, role) +} + +// ReadUserBySessionToken reads a user by session token. +// +// Parameters: +// - ctx: The context of the request. +// - token: The session token to identify the user. +// +// Returns: +// - *storage.User: The user information. +// - error: An error if the operation fails. +func (u *UserService) ReadUserBySessionToken(ctx context.Context, token string) (*storage.User, error) { + session, err := u.sessionProvider.ReadSessionByToken(ctx, token) + if err != nil { + return nil, err + } + + return u.userProvider.ReadUserById(ctx, *session.UserId) +} + +// ReadUser reads a user by ID. +// +// Parameters: +// - ctx: The context of the request. +// - token: The session token to identify the user. +// - id: The ID of the user to read. +// +// Returns: +// - *storage.User: The user information. +// - error: An error if the operation fails. +func (u *UserService) ReadUser(ctx context.Context, token string, id int32) (*storage.User, error) { + _, err := u.ReadUserBySessionToken(ctx, token) + if err != nil { + return nil, err + } + + return u.userProvider.ReadUserById(ctx, id) +} + +// UpdateUser updates a user's information. +// +// Parameters: +// - ctx: The context of the request. +// - token: The session token to identify the user. +// - id: The ID of the user to update. +// - username: The new username (can be nil). +// - password: The new password (can be nil). +// - email: The new email (can be nil). +// - expiresAt: The new expiration time (can be nil). +// - role: The new role (can be nil). +// +// Returns: +// - error: An error if the operation fails. +func (u *UserService) UpdateUser( + ctx context.Context, + token string, + id int32, + username *string, + password *string, + email *string, + expiresAt *time.Time, + role *int32, +) error { + me, err := u.ReadUserBySessionToken(ctx, token) + if err != nil { + return err + } + + user, err := u.userProvider.ReadUserById(ctx, id) + if err != nil { + return err + } + + hasAccess := func() bool { + if me.Id == user.Id { + return false + } + if me.IsAdmin() { + return true + } + if me.IsModerator() && (user.IsParticipant() || user.IsSpectator()) { + return true + } + return false + }() + + if !hasAccess { + return lib.ErrNoPermission + } + + return u.userProvider.UpdateUser(ctx, id, username, password, email, expiresAt, role) +} + +// DeleteUser deletes a user by id. +// +// Parameters: +// - ctx: The context of the request. +// - token: The session token to identify the authenticated user. +// - id: The ID of the user to delete. +// +// Returns: +// - error: An error if the operation fails. +func (u *UserService) DeleteUser(ctx context.Context, token string, id int32) error { + user, err := u.ReadUserBySessionToken(ctx, token) + if err != nil { + return err + } + + if user.Id == id || !user.IsAdmin() { + return lib.ErrNoPermission + } + + return u.userProvider.DeleteUser(ctx, id) +} + +// ReadUserByEmail reads a user by email. +// +// Parameters: +// - ctx: The context of the request. +// - email: The email of the user to read. +// +// Returns: +// - *storage.User: The user information. +// - error: An error if the operation fails. +func (u *UserService) ReadUserByEmail(ctx context.Context, email string) (*storage.User, error) { + return u.userProvider.ReadUserByEmail(ctx, email) +} diff --git a/internal/storage/postgresql.go b/internal/storage/postgresql.go new file mode 100644 index 0000000..e12d4d3 --- /dev/null +++ b/internal/storage/postgresql.go @@ -0,0 +1,267 @@ +package storage + +import ( + "context" + "errors" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" + "ms-auth/internal/lib" + "strings" + "time" + + "github.com/jmoiron/sqlx" +) + +type PostgresqlStorage struct { + db *sqlx.DB + logger *zap.Logger +} + +func NewUserStorage(dsn string, logger *zap.Logger) *PostgresqlStorage { + db, err := sqlx.Connect("pgx", dsn) + if err != nil { + panic(err.Error()) + } + + return &PostgresqlStorage{db: db, logger: logger} +} + +func (storage *PostgresqlStorage) Stop() error { + return storage.db.Close() +} + +const ( + shortUserLifetime = time.Hour * 24 * 30 + defaultUserLifetime = time.Hour * 24 * 365 * 100 +) + +type User struct { + Id int32 `db:"id"` + + Username string `db:"username"` + HashedPassword [60]byte `db:"hashed_pwd"` + + Email *string `db:"email"` + + ExpiresAt time.Time `db:"expires_at"` + CreatedAt time.Time `db:"created_at"` + + Role int32 `db:"role"` +} + +func (user *User) IsAdmin() bool { + return lib.IsAdmin(user.Role) +} + +func (user *User) IsModerator() bool { + return lib.IsModerator(user.Role) +} + +func (user *User) IsParticipant() bool { + return lib.IsParticipant(user.Role) +} + +func (user *User) IsSpectator() bool { + return lib.IsSpectator(user.Role) +} + +func (user *User) AtLeast(role int32) bool { + return user.Role >= role +} + +func (user *User) ComparePassword(password string) error { + if bcrypt.CompareHashAndPassword(user.HashedPassword[:], []byte(password)) != nil { + return lib.ErrBadHandleOrPassword + } + return nil +} + +func (storage *PostgresqlStorage) CreateUser( + ctx context.Context, + username string, + password string, + email *string, + expiresAt *time.Time, + role *int32, +) (*int32, error) { + if err := lib.ValidUsername(username); err != nil { + return nil, err + } + if err := lib.ValidPassword(password); err != nil { + return nil, err + } + if email != nil { + if err := lib.ValidEmail(*email); err != nil { + return nil, err + } + } + if role != nil { + if err := lib.ValidRole(*role); err != nil { + return nil, err + } + } + + username = strings.ToLower(username) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + storage.logger.Error(err.Error()) + return nil, lib.ErrInternal + } + now := time.Now() + username = strings.ToLower(username) + if email != nil { + *email = strings.ToLower(*email) + } + if role == nil { + role = lib.AsInt32P(lib.RoleSpectator) + } + if expiresAt == nil { + if email == nil { + expiresAt = lib.AsTimeP(now.Add(shortUserLifetime)) + } else { + expiresAt = lib.AsTimeP(now.Add(defaultUserLifetime)) + } + } + + query := storage.db.Rebind(` +INSERT INTO users + (username, hashed_pwd, email, expires_at, role) +VALUES (?, ?, ?, ?, ?) +RETURNING id +`) + + rows, err := storage.db.QueryxContext(ctx, query, username, hashedPassword, email, expiresAt, role) + if err != nil { + return nil, storage.handlePgErr(err) + } + defer rows.Close() + var id int32 + err = rows.StructScan(&id) + if err != nil { + return nil, storage.handlePgErr(err) + } + return &id, nil +} +func (storage *PostgresqlStorage) ReadUserByEmail(ctx context.Context, email string) (*User, error) { + if err := lib.ValidEmail(email); err != nil { + return nil, err + } + + email = strings.ToLower(email) + + var user User + query := storage.db.Rebind("SELECT * from users WHERE email=? LIMIT 1") + err := storage.db.GetContext(ctx, &user, query, email) + if err != nil { + return nil, storage.handlePgErr(err) + } + return &user, nil +} +func (storage *PostgresqlStorage) ReadUserByUsername(ctx context.Context, username string) (*User, error) { + if err := lib.ValidUsername(username); err != nil { + return nil, err + } + + username = strings.ToLower(username) + + var user User + query := storage.db.Rebind("SELECT * from users WHERE username=? LIMIT 1") + err := storage.db.GetContext(ctx, &user, query, username) + if err != nil { + return nil, storage.handlePgErr(err) + } + return &user, nil +} +func (storage *PostgresqlStorage) ReadUserById(ctx context.Context, id int32) (*User, error) { + var user User + query := storage.db.Rebind("SELECT * from users WHERE id=? LIMIT 1") + err := storage.db.GetContext(ctx, &user, query, id) + if err != nil { + return nil, storage.handlePgErr(err) + } + return &user, nil +} + +func (storage *PostgresqlStorage) UpdateUser( + ctx context.Context, + id int32, + username *string, + password *string, + email *string, + expiresAt *time.Time, + role *int32, +) error { + var err error + if username != nil { + if err = lib.ValidUsername(*username); err != nil { + return err + } + } + var hashedPassword []byte + if password != nil { + if err = lib.ValidPassword(*password); err != nil { + return err + } + hashedPassword, err = bcrypt.GenerateFromPassword([]byte(*password), bcrypt.DefaultCost) + if err != nil { + storage.logger.Error(err.Error()) + return lib.ErrInternal + } + } + if email != nil { + if err = lib.ValidEmail(*email); err != nil { + return err + } + } + if role != nil { + if err = lib.ValidRole(*role); err != nil { + return err + } + } + + if username != nil { + *username = strings.ToLower(*username) + } + if email != nil { + *email = strings.ToLower(*email) + } + + query := storage.db.Rebind(` +UPDATE users +SET username = COALESCE(?, username), + hashed_pwd = COALESCE(?, hashed_pwd), + email = COALESCE(?, email), + expires_at = COALESCE(?, expires_at), + role = COALESCE(?, role) +WHERE id = ?`) + + _, err = storage.db.ExecContext(ctx, query, username, hashedPassword, email, expiresAt, role, id) + if err != nil { + return storage.handlePgErr(err) + } + return nil +} +func (storage *PostgresqlStorage) DeleteUser(ctx context.Context, id int32) error { + query := storage.db.Rebind("UPDATE users SET expired_at=NOW() WHERE id = ?") + _, err := storage.db.ExecContext(ctx, query, id) + if err != nil { + return storage.handlePgErr(err) + } + + return nil +} + +func (storage *PostgresqlStorage) handlePgErr(err error) error { + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) { + storage.logger.DPanic("unexpected error from postgres", zap.String("err", err.Error())) + return lib.ErrUnexpected + } + if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) { + return errors.New("unique key violation") // FIXME + } + storage.logger.DPanic("unexpected internal error from postgres", zap.String("err", err.Error())) + return lib.ErrInternal +} diff --git a/internal/storage/valkey.go b/internal/storage/valkey.go new file mode 100644 index 0000000..c6e8142 --- /dev/null +++ b/internal/storage/valkey.go @@ -0,0 +1,332 @@ +package storage + +import ( + "context" + "encoding/json" + "errors" + "go.uber.org/zap" + "time" + + "ms-auth/internal/lib" + + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "github.com/valkey-io/valkey-go" + "github.com/valkey-io/valkey-go/valkeylock" +) + +type ValkeyStorage struct { + db valkey.Client + locker valkeylock.Locker + cfg *lib.Config + logger *zap.Logger +} + +func NewValkeyStorage(dsn string, cfg *lib.Config, logger *zap.Logger) *ValkeyStorage { + opts, err := valkey.ParseURL(dsn) + if err != nil { + panic(err.Error()) + } + + db, err := valkey.NewClient(opts) + if err != nil { + panic(err.Error()) + } + + locker, err := valkeylock.NewLocker(valkeylock.LockerOption{ + ClientOption: opts, + KeyMajority: 1, + NoLoopTracking: true, + }) + if err != nil { + panic(err.Error()) + } + + return &ValkeyStorage{ + db: db, + locker: locker, + cfg: cfg, + logger: logger, + } +} + +func (storage *ValkeyStorage) Stop() error { + storage.db.Close() + storage.locker.Close() + return nil +} + +const ( + sessionLifetime = time.Minute * 40 + confirmationLifetime = time.Hour * 5 +) + +func (storage *ValkeyStorage) CreateSession( + ctx context.Context, + user_id int32, +) error { + session := NewSession(user_id) + + resp := storage.db.Do(ctx, storage.db. + B().Set(). + Key(string(*session.UserId)). + Value(*session.Id). + Nx(). + Exat(time.Now().Add(sessionLifetime)). + Build(), + ) + + if err := resp.Error(); err != nil { + storage.logger.Error(err.Error()) + return lib.ErrInternal + } + + return nil +} + +func (storage *ValkeyStorage) ReadSessionByToken(ctx context.Context, token string) (*Session, error) { + session, err := Parse(token, storage.cfg.JWTSecret) + if err != nil { + storage.logger.Error(err.Error()) + return nil, err + } + + real_session, err := storage.ReadSessionByUserId(ctx, *session.UserId) + if err != nil { + storage.logger.Error(err.Error()) + return nil, err + } + + if *session.Id != *real_session.Id { + storage.logger.Error(err.Error()) + return nil, lib.ErrInternal + } + + return session, err +} + +func (storage *ValkeyStorage) ReadSessionByUserId(ctx context.Context, user_id int32) (*Session, error) { + resp := storage.db.Do(ctx, storage.db.B().Get().Key(string(user_id)).Build()) + if err := resp.Error(); err != nil { + storage.logger.Error(err.Error()) + return nil, lib.ErrInternal + } + + id, err := resp.ToString() + if err != nil { + storage.logger.Error(err.Error()) + return nil, lib.ErrInternal + } + + return &Session{ + Id: &id, + UserId: &user_id, + }, err +} + +func (storage *ValkeyStorage) UpdateSession(ctx context.Context, session *Session) error { + resp := storage.db.Do(ctx, storage.db. + B().Set(). + Key(string(*session.UserId)). + Value(*session.Id). + Xx(). + Exat(time.Now().Add(sessionLifetime)). + Build(), + ) + + if err := resp.Error(); err != nil { + storage.logger.Error(err.Error()) + return lib.ErrInternal + } + + return nil +} + +func (storage *ValkeyStorage) DeleteSessionByToken(ctx context.Context, token string) error { + session, err := Parse(token, storage.cfg.JWTSecret) + if err != nil { + storage.logger.Error(err.Error()) + return err + } + + err = storage.DeleteSessionByUserId(ctx, *session.UserId) + if err != nil { + storage.logger.Error(err.Error()) + return err + } + + return nil +} + +func (storage *ValkeyStorage) DeleteSessionByUserId(ctx context.Context, user_id int32) error { + resp := storage.db.Do(ctx, storage.db. + B().Del(). + Key(string(user_id)). + Build(), + ) + + if err := resp.Error(); err != nil { + storage.logger.Error(err.Error()) + return lib.ErrInternal + } + + return nil +} + +func (storage *ValkeyStorage) CreateConfirmation(ctx context.Context, conf *Confirmation) error { + resp := storage.db.Do(ctx, storage.db. + B().Set(). + Key(*conf.Id). + Value(string(conf.JSON())). + Exat(time.Now().Add(confirmationLifetime)). + Build(), + ) + + if err := resp.Error(); err != nil { + storage.logger.Error(err.Error()) + return lib.ErrInternal + } + + return nil +} + +func (storage *ValkeyStorage) ReadConfirmation(ctx context.Context, conf_id string) (*Confirmation, error) { + resp := storage.db.Do(ctx, storage.db. + B().Get(). + Key(conf_id). + Build(), + ) + + if err := resp.Error(); err != nil { + storage.logger.Error(err.Error()) + return nil, lib.ErrInternal + } + + b, err := resp.AsBytes() + if err != nil { + storage.logger.Error(err.Error()) + return nil, lib.ErrInternal + } + + var conf Confirmation + err = json.Unmarshal(b, &conf) + if err != nil { + storage.logger.Error(err.Error()) + return nil, lib.ErrInternal + } + + return &conf, nil +} + +func (storage *ValkeyStorage) DeleteConfirmation(ctx context.Context, conf_id string) error { + resp := storage.db.Do(ctx, storage.db. + B().Del(). + Key(conf_id). + Build(), + ) + + if err := resp.Error(); err != nil { + storage.logger.Error(err.Error()) + return lib.ErrInternal + } + + return nil +} + +var ( + ErrBadSession = errors.New("bad session") + ErrBadConfirmation = errors.New("bad confirmation") +) + +type Confirmation struct { + Id *string `json:"id"` + UserId *int32 `json:"user_id,omitempty"` + Email *string `json:"email"` +} + +func NewConfirmation(userId *int32, email string) (*Confirmation, error) { + c := &Confirmation{ + Id: lib.AsStringP(uuid.NewString()), + UserId: userId, + Email: &email, + } + + if err := c.Valid(); err != nil { + return nil, err + } + + return c, nil +} + +func (c *Confirmation) Valid() error { + if c.Id == nil { + return ErrBadConfirmation + } + // FIXME + // if c.userId == nil { + // return ErrBadConfirmation + // } + if c.Email == nil { + return ErrBadConfirmation + } + if err := lib.ValidEmail(*c.Email); err != nil { + return err + } + return nil +} + +func (c *Confirmation) JSON() []byte { + b, err := json.Marshal(c) + if err != nil { + panic(err.Error()) + } + return b +} + +type Session struct { + Id *string + UserId *int32 +} + +func NewSession(userId int32) *Session { + return &Session{ + Id: lib.AsStringP(uuid.NewString()), + UserId: &userId, + } +} + +func (s Session) Valid() error { + if s.Id == nil { + return ErrBadSession + } + if s.UserId == nil { + return ErrBadSession + } + return nil +} + +func (s Session) Token(secret string) (string, error) { + if err := s.Valid(); err != nil { + return "", err + } + refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, s) + str, err := refreshToken.SignedString([]byte(secret)) + if err != nil { + return "", ErrBadSession + } + return str, nil +} + +func Parse(tkn string, secret string) (*Session, error) { + parsedToken, err := jwt.ParseWithClaims(tkn, &Session{}, func(token *jwt.Token) (interface{}, error) { + return []byte(secret), nil + }) + if err != nil { + return nil, ErrBadSession + } + session := parsedToken.Claims.(*Session) + if err := session.Valid(); err != nil { + return nil, err + } + return session, nil +} diff --git a/internal/transport/email_server.go b/internal/transport/email_server.go new file mode 100644 index 0000000..bbc926c --- /dev/null +++ b/internal/transport/email_server.go @@ -0,0 +1,11 @@ +package transport + +import ( + "context" + "google.golang.org/protobuf/types/known/emptypb" + emailv1 "ms-auth/pkg/go/gen/email/v1" +) + +func (s *AuthServer) SendEmail(ctx context.Context, req *emailv1.SendEmailRequest) (*emptypb.Empty, error) { + panic("not implemented") +} diff --git a/internal/transport/interceptors.go b/internal/transport/interceptors.go new file mode 100644 index 0000000..d11d0be --- /dev/null +++ b/internal/transport/interceptors.go @@ -0,0 +1 @@ +package transport diff --git a/internal/transport/server.go b/internal/transport/server.go new file mode 100644 index 0000000..0a887a3 --- /dev/null +++ b/internal/transport/server.go @@ -0,0 +1,123 @@ +package transport + +import ( + "context" + "go.uber.org/zap" + "google.golang.org/protobuf/types/known/timestamppb" + "ms-auth/internal/storage" + emailv1 "ms-auth/pkg/go/gen/email/v1" + sessionv1 "ms-auth/pkg/go/gen/session/v1" + userv1 "ms-auth/pkg/go/gen/user/v1" + "net" + "time" + + "google.golang.org/grpc" +) + +type SessionServiceI interface { + Create(ctx context.Context, handle, password string) (*string, error) + Read(ctx context.Context, token string) (*int32, error) + Update(ctx context.Context, token string) error + Delete(ctx context.Context, token string) error +} + +type UserServiceI interface { + CreateUser(ctx context.Context, token, username, password string, email *string, expiresAt *time.Time, role *int32) (*int32, error) + ReadUser(ctx context.Context, token string, id int32) (*storage.User, error) + UpdateUser(ctx context.Context, token string, id int32, username *string, password *string, email *string, expiresAt *time.Time, role *int32) error + DeleteUser(ctx context.Context, token string, id int32) error +} + +type AuthServer struct { + emailv1.UnimplementedEmailServiceServer + + sessionv1.UnimplementedSessionServiceServer + sessionService SessionServiceI + + userv1.UnimplementedUserServiceServer + userService UserServiceI + + gRPCServer *grpc.Server + logger *zap.Logger +} + +// NewAuthServer creates a new instance of the AuthServer struct. +// +// Parameters: +// - sessionService: A pointer to the SessionServiceI interface. +// - gRPCServer: A pointer to the grpc.Server struct. +// - logger: A pointer to the zap.Logger struct. +// +// Returns: +// - *AuthServer: A pointer to the AuthServer struct. +func NewAuthServer(sessionService SessionServiceI, userService UserServiceI, gRPCServer *grpc.Server, logger *zap.Logger) *AuthServer { + return &AuthServer{ + sessionService: sessionService, + userService: userService, + gRPCServer: gRPCServer, + logger: logger, + } +} + +// Start starts the AuthServer and listens on port :8090. +// +// It creates a listener on the specified address and starts serving incoming requests. +// It also logs the server start and any errors that occur during serving. +// +// No parameters. +// No return values. +func (s *AuthServer) Start() { + lis, err := net.Listen("tcp", ":8090") + if err != nil { + s.logger.Fatal("") + } + + sessionv1.RegisterSessionServiceServer(s.gRPCServer, s) + go func() { + s.logger.Info("Listening on :8090") + if err := s.gRPCServer.Serve(lis); err != nil { + panic(err.Error()) + } + }() + s.logger.Info("server started") +} + +// GracefullyStop stops the server gracefully. +// +// No parameters. +// No return values. +func (s *AuthServer) GracefullyStop() { + s.gRPCServer.GracefulStop() + s.logger.Info("server stopped") +} + +func AsTimeP(t *timestamppb.Timestamp) *time.Time { + if t == nil { + return nil + } + tt := t.AsTime() + return &tt +} + +func AsInt32P(v *userv1.Role) *int32 { + if v == nil { + return nil + } + vv := int32(v.Number()) + return &vv +} + +func AsTimestampP(t *time.Time) *timestamppb.Timestamp { + if t == nil { + return nil + } + return timestamppb.New(*t) +} + +func AsRoleP(r *int32) *userv1.Role { + if r == nil { + return nil + } + rr := userv1.Role(*r) + return &rr +} diff --git a/internal/transport/session_server.go b/internal/transport/session_server.go new file mode 100644 index 0000000..4cc2560 --- /dev/null +++ b/internal/transport/session_server.go @@ -0,0 +1,45 @@ +package transport + +import ( + "context" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + sessionv1 "ms-auth/pkg/go/gen/session/v1" +) + +func (s *AuthServer) Create(ctx context.Context, req *sessionv1.CreateSessionRequest) (*sessionv1.CreateSessionResponse, error) { + token, err := s.sessionService.Create(ctx, req.GetHandle(), req.GetPassword()) + if err != nil { + return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME + } + return &sessionv1.CreateSessionResponse{ + Token: *token, + }, nil +} + +func (s *AuthServer) Read(ctx context.Context, req *sessionv1.ReadSessionRequest) (*sessionv1.ReadSessionResponse, error) { + id, err := s.sessionService.Read(ctx, req.GetToken()) + if err != nil { + return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME + } + return &sessionv1.ReadSessionResponse{ + UserId: *id, + }, nil +} + +func (s *AuthServer) Update(ctx context.Context, req *sessionv1.UpdateSessionRequest) (*emptypb.Empty, error) { + err := s.sessionService.Update(ctx, req.GetToken()) + if err != nil { + return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME + } + return &emptypb.Empty{}, nil +} + +func (s *AuthServer) Delete(ctx context.Context, req *sessionv1.DeleteSessionRequest) (*emptypb.Empty, error) { + err := s.sessionService.Delete(ctx, req.GetToken()) + if err != nil { + return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME + } + return &emptypb.Empty{}, nil +} diff --git a/internal/transport/user_server.go b/internal/transport/user_server.go new file mode 100644 index 0000000..8d39245 --- /dev/null +++ b/internal/transport/user_server.go @@ -0,0 +1,124 @@ +package transport + +import ( + "context" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + userv1 "ms-auth/pkg/go/gen/user/v1" + "strings" +) + +func (s *AuthServer) CreateUser(ctx context.Context, req *userv1.CreateUserRequest) (*userv1.CreateUserResponse, error) { + user := req.GetUser() + if user == nil { + return nil, status.Errorf(codes.Unknown, "") // FIXME + } + id, err := s.userService.CreateUser( + ctx, + req.GetToken(), + user.GetUsername(), + user.GetPassword(), + user.Email, + AsTimeP(user.ExpiresAt), + AsInt32P(user.Role), + ) + if err != nil { + return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME + } + + return &userv1.CreateUserResponse{ + Id: *id, + }, nil +} + +func (s *AuthServer) ReadUser(ctx context.Context, req *userv1.ReadUserRequest) (*userv1.ReadUserResponse, error) { + user, err := s.userService.ReadUser( + ctx, + req.GetToken(), + req.GetId(), + ) + if err != nil { + return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME + } + + return &userv1.ReadUserResponse{ + User: &userv1.ReadUserResponse_User{ + Id: user.Id, + Username: user.Username, + Email: user.Email, + ExpiresAt: AsTimestampP(&user.ExpiresAt), + CreatedAt: AsTimestampP(&user.CreatedAt), + Role: *AsRoleP(&user.Role), + }, + }, nil +} + +func (s *AuthServer) UpdateUser(ctx context.Context, req *userv1.UpdateUserRequest) (*emptypb.Empty, error) { + user := req.GetUser() + if user == nil { + return nil, status.Errorf(codes.Unknown, "") // FIXME + } + err := s.userService.UpdateUser( + ctx, + req.GetToken(), + user.GetId(), + user.Username, + user.Password, + user.Email, + AsTimeP(user.ExpiresAt), + AsInt32P(user.Role), + ) + if err != nil { + return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME + } + return &emptypb.Empty{}, nil +} + +func (s *AuthServer) DeleteUser(ctx context.Context, req *userv1.DeleteUserRequest) (*emptypb.Empty, error) { + err := s.userService.DeleteUser( + ctx, + req.GetToken(), + req.GetId(), + ) + if err != nil { + return nil, status.Errorf(codes.Unknown, err.Error()) // FIXME + } + return &emptypb.Empty{}, nil +} + +func (s *AuthServer) ConfirmEmail(ctx context.Context, req *userv1.ConfirmEmailRequest) (*emptypb.Empty, error) { + panic("not implemented") +} + +func (s *AuthServer) RegisterUser(ctx context.Context, req *userv1.RegisterUserRequest) (*emptypb.Empty, error) { + panic("not implemented") +} + +func (s *AuthServer) ConfirmRegisterUser(ctx context.Context, req *userv1.ConfirmRegisterUserRequest) (*emptypb.Empty, error) { + panic("not implemented") +} + +func (s *AuthServer) ResetPassword(ctx context.Context, req *userv1.ResetPasswordRequest) (*emptypb.Empty, error) { + panic("not implemented") +} + +func (s *AuthServer) ConfirmResetPassword(ctx context.Context, req *userv1.ConfirmResetPasswordRequest) (*emptypb.Empty, error) { + panic("not implemented") +} + +func shortenEmail(email *string) *string { + if email == nil { + return nil + } + parts := strings.Split(*email, "@") + p1 := parts[0] + p2 := parts[1] + a := "****" + if len(p1) <= 4 { + e := a + "@" + p2 + return &e + } + e := p1[:len(p1)-4] + a + "@" + p2 + return &e +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..22a5e79 --- /dev/null +++ b/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "go.uber.org/zap" + "google.golang.org/grpc" + "ms-auth/internal/app" + "ms-auth/internal/lib" + "ms-auth/internal/services" + "ms-auth/internal/storage" + "ms-auth/internal/transport" + "os" + "os/signal" + "syscall" +) + +func main() { + cfg := lib.MustSetupConfig() + + var logger *zap.Logger + if cfg.Env == "prod" { + logger = zap.Must(zap.NewProduction()) + } else if cfg.Env == "dev" { + logger = zap.Must(zap.NewDevelopment()) + } else { + panic(fmt.Sprintf(`error reading config: env expected "prod" or "dev", got "%s"`, cfg.Env)) + } + + postgres := storage.NewUserStorage(cfg.PostgresDSN, logger) + + vk := storage.NewValkeyStorage(cfg.RedisDSN, cfg, logger) + + sessionService := services.NewSessionService(vk, postgres, cfg) + userService := services.NewUserService(postgres, vk, vk, cfg) + + server := transport.NewAuthServer(sessionService, userService, grpc.NewServer(), logger) + + application := app.NewApp(cfg, server) + + application.Start() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) + + <-stop + application.GracefullyStop() +} diff --git a/migrations/20240608163806_initial.sql b/migrations/20240608163806_initial.sql new file mode 100644 index 0000000..bd150c0 --- /dev/null +++ b/migrations/20240608163806_initial.sql @@ -0,0 +1,43 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS users +( + id serial NOT NULL, + username VARCHAR(70) UNIQUE NOT NULL, + hashed_pwd VARCHAR(60) NOT NULL, + email VARCHAR(70) UNIQUE, + role INT NOT NULL, + expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + PRIMARY KEY (id), + CHECK (length(username) != 0 AND username = lower(username)), + CHECK (length(email) != 0 AND email = lower(email)), + CHECK (lower(username) != lower(email)), + CHECK (length(hashed_pwd) != 0), + CHECK (role BETWEEN 0 AND 3) +); + +CREATE INDEX ON users (id); +CREATE INDEX ON users (username); +CREATE INDEX ON users (email); + +CREATE FUNCTION usr_upd_trg_fn() RETURNS TRIGGER + LANGUAGE plpgsql AS +$$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$; + +CREATE TRIGGER usr_upd_trg + BEFORE UPDATE + ON users + FOR EACH ROW +EXECUTE PROCEDURE usr_upd_trg_fn(); +-- +goose StatementEnd + +-- +goose Down +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/pkg/go/gen/user/v1/user.pb.go b/pkg/go/gen/user/v1/user.pb.go new file mode 100644 index 0000000..4ccb005 --- /dev/null +++ b/pkg/go/gen/user/v1/user.pb.go @@ -0,0 +1,412 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: user/v1/user.proto + +package userv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +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 Role int32 + +const ( + Role_ROLE_SPECTATOR_UNSPECIFIED Role = 0 + Role_ROLE_PARTICIPANT Role = 1 + Role_ROLE_MODERATOR Role = 2 + Role_ROLE_ADMIN Role = 3 +) + +// Enum value maps for Role. +var ( + Role_name = map[int32]string{ + 0: "ROLE_SPECTATOR_UNSPECIFIED", + 1: "ROLE_PARTICIPANT", + 2: "ROLE_MODERATOR", + 3: "ROLE_ADMIN", + } + Role_value = map[string]int32{ + "ROLE_SPECTATOR_UNSPECIFIED": 0, + "ROLE_PARTICIPANT": 1, + "ROLE_MODERATOR": 2, + "ROLE_ADMIN": 3, + } +) + +func (x Role) Enum() *Role { + p := new(Role) + *p = x + return p +} + +func (x Role) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Role) Descriptor() protoreflect.EnumDescriptor { + return file_user_v1_user_proto_enumTypes[0].Descriptor() +} + +func (Role) Type() protoreflect.EnumType { + return &file_user_v1_user_proto_enumTypes[0] +} + +func (x Role) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Role.Descriptor instead. +func (Role) EnumDescriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{0} +} + +type CreateUserRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + User *CreateUserRequest_User `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` +} + +func (x *CreateUserRequest) Reset() { + *x = CreateUserRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_user_v1_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserRequest) ProtoMessage() {} + +func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead. +func (*CreateUserRequest) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateUserRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *CreateUserRequest) GetUser() *CreateUserRequest_User { + if x != nil { + return x.User + } + return nil +} + +type CreateUserResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *CreateUserResponse) Reset() { + *x = CreateUserResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_user_v1_user_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserResponse) ProtoMessage() {} + +func (x *CreateUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateUserResponse.ProtoReflect.Descriptor instead. +func (*CreateUserResponse) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateUserResponse) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +type CreateUserRequest_User struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + Email *string `protobuf:"bytes,3,opt,name=email,proto3,oneof" json:"email,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3,oneof" json:"expires_at,omitempty"` + Role *Role `protobuf:"varint,5,opt,name=role,proto3,enum=proto.user.v1.Role,oneof" json:"role,omitempty"` +} + +func (x *CreateUserRequest_User) Reset() { + *x = CreateUserRequest_User{} + if protoimpl.UnsafeEnabled { + mi := &file_user_v1_user_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateUserRequest_User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserRequest_User) ProtoMessage() {} + +func (x *CreateUserRequest_User) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateUserRequest_User.ProtoReflect.Descriptor instead. +func (*CreateUserRequest_User) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *CreateUserRequest_User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *CreateUserRequest_User) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *CreateUserRequest_User) GetEmail() string { + if x != nil && x.Email != nil { + return *x.Email + } + return "" +} + +func (x *CreateUserRequest_User) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +func (x *CreateUserRequest_User) GetRole() Role { + if x != nil && x.Role != nil { + return *x.Role + } + return Role_ROLE_SPECTATOR_UNSPECIFIED +} + +var File_user_v1_user_proto protoreflect.FileDescriptor + +var file_user_v1_user_proto_rawDesc = []byte{ + 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0xd0, 0x02, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x39, 0x0a, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x55, 0x73, + 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x1a, 0xe9, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, + 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x19, 0x0a, 0x05, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, + 0x6c, 0x88, 0x01, 0x01, 0x12, 0x3e, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, + 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x48, 0x01, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, + 0x74, 0x88, 0x01, 0x01, 0x12, 0x2c, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x48, 0x02, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x88, + 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x42, 0x0d, 0x0a, 0x0b, + 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x5f, + 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x24, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, + 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x2a, 0x60, 0x0a, 0x04, 0x52, 0x6f, + 0x6c, 0x65, 0x12, 0x1e, 0x0a, 0x1a, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x53, 0x50, 0x45, 0x43, 0x54, + 0x41, 0x54, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x50, 0x41, 0x52, 0x54, 0x49, + 0x43, 0x49, 0x50, 0x41, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x4f, 0x4c, 0x45, + 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x52, 0x41, 0x54, 0x4f, 0x52, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, + 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x41, 0x44, 0x4d, 0x49, 0x4e, 0x10, 0x03, 0x32, 0x60, 0x0a, 0x0b, + 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0a, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x97, + 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x21, 0x6d, 0x73, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67, + 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, + 0x65, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x50, 0x55, 0x58, 0xaa, 0x02, 0x0d, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0d, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x5c, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x19, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x5c, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x3a, 0x3a, + 0x55, 0x73, 0x65, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_user_v1_user_proto_rawDescOnce sync.Once + file_user_v1_user_proto_rawDescData = file_user_v1_user_proto_rawDesc +) + +func file_user_v1_user_proto_rawDescGZIP() []byte { + file_user_v1_user_proto_rawDescOnce.Do(func() { + file_user_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(file_user_v1_user_proto_rawDescData) + }) + return file_user_v1_user_proto_rawDescData +} + +var file_user_v1_user_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_user_v1_user_proto_goTypes = []interface{}{ + (Role)(0), // 0: proto.user.v1.Role + (*CreateUserRequest)(nil), // 1: proto.user.v1.CreateUserRequest + (*CreateUserResponse)(nil), // 2: proto.user.v1.CreateUserResponse + (*CreateUserRequest_User)(nil), // 3: proto.user.v1.CreateUserRequest.User + (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp +} +var file_user_v1_user_proto_depIdxs = []int32{ + 3, // 0: proto.user.v1.CreateUserRequest.user:type_name -> proto.user.v1.CreateUserRequest.User + 4, // 1: proto.user.v1.CreateUserRequest.User.expires_at:type_name -> google.protobuf.Timestamp + 0, // 2: proto.user.v1.CreateUserRequest.User.role:type_name -> proto.user.v1.Role + 1, // 3: proto.user.v1.UserService.CreateUser:input_type -> proto.user.v1.CreateUserRequest + 2, // 4: proto.user.v1.UserService.CreateUser:output_type -> proto.user.v1.CreateUserResponse + 4, // [4:5] is the sub-list for method output_type + 3, // [3:4] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_user_v1_user_proto_init() } +func file_user_v1_user_proto_init() { + if File_user_v1_user_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_user_v1_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateUserRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_user_v1_user_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateUserResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_user_v1_user_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateUserRequest_User); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_user_v1_user_proto_msgTypes[2].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_user_v1_user_proto_rawDesc, + NumEnums: 1, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_user_v1_user_proto_goTypes, + DependencyIndexes: file_user_v1_user_proto_depIdxs, + EnumInfos: file_user_v1_user_proto_enumTypes, + MessageInfos: file_user_v1_user_proto_msgTypes, + }.Build() + File_user_v1_user_proto = out.File + file_user_v1_user_proto_rawDesc = nil + file_user_v1_user_proto_goTypes = nil + file_user_v1_user_proto_depIdxs = nil +} diff --git a/pkg/go/gen/user/v1/user_grpc.pb.go b/pkg/go/gen/user/v1/user_grpc.pb.go new file mode 100644 index 0000000..b2a0bb2 --- /dev/null +++ b/pkg/go/gen/user/v1/user_grpc.pb.go @@ -0,0 +1,105 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc (unknown) +// source: user/v1/user.proto + +package userv1 + +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.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// UserServiceClient is the client API for UserService 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 UserServiceClient interface { + CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) +} + +type userServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { + return &userServiceClient{cc} +} + +func (c *userServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) { + out := new(CreateUserResponse) + err := c.cc.Invoke(ctx, "/proto.user.v1.UserService/CreateUser", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// UserServiceServer is the server API for UserService service. +// All implementations must embed UnimplementedUserServiceServer +// for forward compatibility +type UserServiceServer interface { + CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) + mustEmbedUnimplementedUserServiceServer() +} + +// UnimplementedUserServiceServer must be embedded to have forward compatible implementations. +type UnimplementedUserServiceServer struct { +} + +func (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") +} +func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} + +// UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to UserServiceServer will +// result in compilation errors. +type UnsafeUserServiceServer interface { + mustEmbedUnimplementedUserServiceServer() +} + +func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { + s.RegisterService(&UserService_ServiceDesc, srv) +} + +func _UserService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).CreateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.user.v1.UserService/CreateUser", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var UserService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proto.user.v1.UserService", + HandlerType: (*UserServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateUser", + Handler: _UserService_CreateUser_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "user/v1/user.proto", +} diff --git a/proto/user/v1/user.proto b/proto/user/v1/user.proto new file mode 100644 index 0000000..03981ea --- /dev/null +++ b/proto/user/v1/user.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package proto.user.v1; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +service UserService { + rpc CreateUser (CreateUserRequest) returns (CreateUserResponse); +} + +enum Role { + ROLE_SPECTATOR_UNSPECIFIED = 0; + ROLE_PARTICIPANT = 1; + ROLE_MODERATOR = 2; + ROLE_ADMIN = 3; +} + +message CreateUserRequest { + message User { + string username = 1; + string password = 2; + optional string email = 3; + optional google.protobuf.Timestamp expires_at = 4; + optional Role role = 5; + } + + string token = 1; + User user = 2; +} +message CreateUserResponse { + int32 id = 1; +}