Initial commit

This commit is contained in:
new_gate 2024-07-27 07:31:04 +00:00
commit 2fa110e760
28 changed files with 2346 additions and 0 deletions

9
Dockerfile Normal file
View file

@ -0,0 +1,9 @@
FROM golang:latest
WORKDIR /app
COPY . .
RUN go build ./main.go
CMD ["./main"]

4
Makefile Normal file
View file

@ -0,0 +1,4 @@
all:
go build
bug generate proto

12
buf.gen.yaml Normal file
View file

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

7
buf.yaml Normal file
View file

@ -0,0 +1,7 @@
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT

75
docker-compose.yaml Normal file
View file

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

37
go.mod Normal file
View file

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

80
go.sum Normal file
View file

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

35
internal/app/app.go Normal file
View file

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

27
internal/lib/config.go Normal file
View file

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

22
internal/lib/errors.go Normal file
View file

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

40
internal/lib/lib.go Normal file
View file

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

19
internal/lib/mail.go Normal file
View file

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

View file

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

View file

@ -0,0 +1 @@
package services

View file

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

244
internal/services/user.go Normal file
View file

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

View file

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

332
internal/storage/valkey.go Normal file
View file

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

View file

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

View file

@ -0,0 +1 @@
package transport

View file

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

View file

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

View file

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

47
main.go Normal file
View file

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

View file

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

View file

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

View file

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

33
proto/user/v1/user.proto Normal file
View file

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