add auth
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
ARG GOARCH=amd64
|
||||||
|
ARG GOOS=linux
|
||||||
|
ARG CGO_ENABLED=0
|
||||||
|
ARG BUILD_TIME=unknown
|
||||||
|
ARG VERSION=unknown
|
||||||
|
|
||||||
|
# Stage 1: Build
|
||||||
|
FROM docker.io/golang:1.24-alpine AS build
|
||||||
|
|
||||||
|
ARG GOOS
|
||||||
|
ARG GOARCH
|
||||||
|
ARG CGO_ENABLED
|
||||||
|
ARG BUILD_TIME
|
||||||
|
ARG VERSION
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
CGO_ENABLED=${CGO_ENABLED} \
|
||||||
|
GOOS=${GOOS} \
|
||||||
|
GOARCH=${GOARCH} \
|
||||||
|
go build -trimpath \
|
||||||
|
-ldflags "-s -w -X main.BuildTime=${BUILD_TIME} -X main.Version=${VERSION}" \
|
||||||
|
-o /out/auth ./cmd/auth && \
|
||||||
|
chmod +x /out/auth
|
||||||
|
|
||||||
|
# Stage 2: Runtime
|
||||||
|
FROM gcr.io/distroless/static-debian13:latest AS runtime
|
||||||
|
|
||||||
|
ARG BUILD_TIME
|
||||||
|
ARG VERSION
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build --chown=1000:1000 /out/auth /app/bin/auth
|
||||||
|
|
||||||
|
EXPOSE 8081 50052
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/bin/auth"]
|
||||||
@@ -10,10 +10,10 @@ BINARY_DIR=bin
|
|||||||
# Protobuf parameters
|
# Protobuf parameters
|
||||||
PROTOC=protoc
|
PROTOC=protoc
|
||||||
PROTO_DIR=api/proto
|
PROTO_DIR=api/proto
|
||||||
PROTO_FILE=$(PROTO_DIR)/auth.proto $(PROTO_DIR)/competition.proto $(PROTO_DIR)/task.proto
|
PROTO_FILE=$(PROTO_DIR)/auth.proto $(PROTO_DIR)/competition.proto $(PROTO_DIR)/results.proto $(PROTO_DIR)/review.proto $(PROTO_DIR)/submission.proto $(PROTO_DIR)/task.proto $(PROTO_DIR)/user.proto
|
||||||
PROTO_OUT=.
|
PROTO_OUT=.
|
||||||
|
|
||||||
.PHONY: install i generate gen generate-gw test build run migrate lint fmt format clean help
|
.PHONY: install i generate gen generate-gw test build run migrate lint fmt format clean help codegen examples
|
||||||
|
|
||||||
install:
|
install:
|
||||||
$(GODOWNLOAD)
|
$(GODOWNLOAD)
|
||||||
@@ -65,6 +65,33 @@ format: fmt
|
|||||||
clean:
|
clean:
|
||||||
rm -rf bin/*
|
rm -rf bin/*
|
||||||
|
|
||||||
|
codegen:
|
||||||
|
@if [ -z "$(SERVICE)" ]; then \
|
||||||
|
echo "Usage: make codegen SERVICE=<service-name>"; \
|
||||||
|
echo "Example: make codegen SERVICE=auth"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
$(GOBUILD) -o ./$(BINARY_DIR)/codegen ./cmd/codegen
|
||||||
|
./$(BINARY_DIR)/codegen $(SERVICE) ./
|
||||||
|
|
||||||
|
examples:
|
||||||
|
@echo "📖 Datarush-Go Integration Examples"
|
||||||
|
@echo ""
|
||||||
|
@echo "Files:"
|
||||||
|
@echo " - SQL_BUILDER_ADVANCED.md - Детальные примеры SQL Builder"
|
||||||
|
@echo " - SQL_BUILDER_CODEGEN.md - Документация Codegen"
|
||||||
|
@echo " - SQL_EXAMPLES.sql - SQL схемы и примеры"
|
||||||
|
@echo " - INTEGRATION_EXAMPLES.sh - Интеграционные примеры"
|
||||||
|
@echo ""
|
||||||
|
@echo "Быстрый старт:"
|
||||||
|
@echo " 1. make codegen SERVICE=payment"
|
||||||
|
@echo " 2. Edit internal/payment/repository/postgres/repo.go"
|
||||||
|
@echo " 3. Edit internal/payment/service/service.go"
|
||||||
|
@echo " 4. go test ./internal/payment/..."
|
||||||
|
@echo ""
|
||||||
|
@cat INTEGRATION_EXAMPLES.sh
|
||||||
|
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Available commands:"
|
@echo "Available commands:"
|
||||||
@echo " install - Install all deps using go mod download"
|
@echo " install - Install all deps using go mod download"
|
||||||
@@ -72,6 +99,9 @@ help:
|
|||||||
@echo " generate - Generate gRPC code"
|
@echo " generate - Generate gRPC code"
|
||||||
@echo " gen"
|
@echo " gen"
|
||||||
@echo " protoc"
|
@echo " protoc"
|
||||||
|
@echo " codegen - Generate service template"
|
||||||
|
@echo " Usage: make codegen SERVICE=<name>"
|
||||||
|
@echo " examples - Show integration examples"
|
||||||
@echo " test - Run tests"
|
@echo " test - Run tests"
|
||||||
@echo " build - Build the binary"
|
@echo " build - Build the binary"
|
||||||
@echo " run - Run the application"
|
@echo " run - Run the application"
|
||||||
@@ -80,6 +110,7 @@ help:
|
|||||||
@echo " fmt"
|
@echo " fmt"
|
||||||
@echo " clean - Clean build artifacts"
|
@echo " clean - Clean build artifacts"
|
||||||
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
%:
|
%:
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"datarush/internal/auth/config"
|
||||||
|
"datarush/internal/auth/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := server.New(cfg)
|
||||||
|
|
||||||
|
if err := srv.Start(); err != nil {
|
||||||
|
log.Fatalf("failed to start server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
log.Println("shutting down auth server...")
|
||||||
|
srv.Stop()
|
||||||
|
log.Println("auth server stopped")
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS users;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
username VARCHAR(255) NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
@@ -1,6 +1,38 @@
|
|||||||
name: order
|
name: order
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
auth:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Containerfile.auth
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
restart: false
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
env_file:
|
||||||
|
- path: ./infrastructure/auth/.env.template
|
||||||
|
required: true
|
||||||
|
- path: ./infrastructure/auth/.env
|
||||||
|
required: false
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
target: 8081
|
||||||
|
published: 13443
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
app_protocol: http
|
||||||
|
- name: grpc
|
||||||
|
target: 50052
|
||||||
|
published: 13444
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
protocol: tcp
|
||||||
|
app_protocol: http
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
restart: unless-stopped
|
||||||
|
shm_size: 4mb
|
||||||
|
|
||||||
core:
|
core:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -18,6 +50,10 @@ services:
|
|||||||
restart: false
|
restart: false
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
required: true
|
required: true
|
||||||
|
auth:
|
||||||
|
restart: false
|
||||||
|
condition: service_started
|
||||||
|
required: true
|
||||||
env_file:
|
env_file:
|
||||||
- path: ./infrastructure/core/.env.template
|
- path: ./infrastructure/core/.env.template
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ go 1.24.0
|
|||||||
toolchain go1.24.9
|
toolchain go1.24.9
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Masterminds/squirrel v1.5.4
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-playground/validator/v10 v10.28.0
|
github.com/go-playground/validator/v10 v10.28.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.0
|
github.com/golang-migrate/migrate/v4 v4.19.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3
|
||||||
@@ -13,6 +16,7 @@ require (
|
|||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/redis/go-redis/v9 v9.16.0
|
github.com/redis/go-redis/v9 v9.16.0
|
||||||
|
golang.org/x/crypto v0.42.0
|
||||||
google.golang.org/grpc v1.76.0
|
google.golang.org/grpc v1.76.0
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.10
|
||||||
)
|
)
|
||||||
@@ -26,10 +30,11 @@ require (
|
|||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||||
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.42.0 // indirect
|
|
||||||
golang.org/x/net v0.43.0 // indirect
|
golang.org/x/net v0.43.0 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
golang.org/x/text v0.29.0 // indirect
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||||
|
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
@@ -33,6 +35,7 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
@@ -49,6 +52,9 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
|
|||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
@@ -72,6 +78,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||||
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||||
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||||
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
@@ -96,6 +106,7 @@ github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERS
|
|||||||
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
AUTH_GRPC_PORT=50052
|
||||||
|
AUTH_HTTP_PORT=8081
|
||||||
|
AUTH_GRPC_ENABLE_REFLECTION=true
|
||||||
|
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USERNAME=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
|
POSTGRES_DATABASE=postgres
|
||||||
|
|
||||||
|
JWT_SECRET=your-secret-key-change-in-production
|
||||||
|
|
||||||
|
LOG_LEVEL=info
|
||||||
@@ -8,3 +8,4 @@ POSTGRES_PASSWORD=postgres
|
|||||||
POSTGRES_DATABASE=postgres
|
POSTGRES_DATABASE=postgres
|
||||||
|
|
||||||
REDIS_URI=redis://redis:6379
|
REDIS_URI=redis://redis:6379
|
||||||
|
AUTH_GRPC_ADDR=auth:50052
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
GRPCPort int
|
||||||
|
GRPCEnableReflection bool
|
||||||
|
HTTPPort int
|
||||||
|
LogLevel string
|
||||||
|
DBHost string
|
||||||
|
DBPort int
|
||||||
|
DBUser string
|
||||||
|
DBPassword string
|
||||||
|
DBName string
|
||||||
|
JWTSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
_ = godotenv.Load()
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
GRPCPort: mustGetInt("AUTH_GRPC_PORT", 50052),
|
||||||
|
GRPCEnableReflection: mustGetBool("AUTH_GRPC_ENABLE_REFLECTION", false),
|
||||||
|
HTTPPort: mustGetInt("AUTH_HTTP_PORT", 8081),
|
||||||
|
LogLevel: getEnv("LOG_LEVEL", "info"),
|
||||||
|
DBHost: getEnv("POSTGRES_HOST", "localhost"),
|
||||||
|
DBPort: mustGetInt("POSTGRES_PORT", 5432),
|
||||||
|
DBUser: getEnv("POSTGRES_USERNAME", "postgres"),
|
||||||
|
DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"),
|
||||||
|
DBName: getEnv("POSTGRES_DATABASE", "postgres"),
|
||||||
|
JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, def string) string {
|
||||||
|
if val := os.Getenv(key); val != "" {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetInt(key string, def int) int {
|
||||||
|
val := getEnv(key, strconv.Itoa(def))
|
||||||
|
n, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid int for %s: %v", key, err)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetBool(key string, def bool) bool {
|
||||||
|
val := getEnv(key, strconv.FormatBool(def))
|
||||||
|
b, err := strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid bool for %s: %v", key, err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) BuildPostgresConnStr() string {
|
||||||
|
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
c.DBHost, c.DBPort, c.DBUser, c.DBPassword, c.DBName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) BuildPostgresDSN() string {
|
||||||
|
return fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable",
|
||||||
|
c.DBUser, c.DBPassword, net.JoinHostPort(c.DBHost, strconv.Itoa(c.DBPort)), c.DBName)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUserNotFound = errors.New("user not found")
|
||||||
|
ErrInvalidEmail = errors.New("invalid email")
|
||||||
|
ErrInvalidPassword = errors.New("invalid password")
|
||||||
|
ErrUserAlreadyExists = errors.New("user already exists")
|
||||||
|
ErrInvalidToken = errors.New("invalid token")
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type ID = uuid.UUID
|
||||||
|
|
||||||
|
func NewID() ID {
|
||||||
|
return uuid.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseID(s string) (ID, error) {
|
||||||
|
return uuid.Parse(s)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID ID
|
||||||
|
Email string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserWithoutPassword struct {
|
||||||
|
ID ID
|
||||||
|
Email string
|
||||||
|
Username string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"datarush/internal/auth/domain"
|
||||||
|
"datarush/internal/auth/service"
|
||||||
|
pb "datarush/pkg/api/auth"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
pb.UnimplementedAuthServiceServer
|
||||||
|
authService *service.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
|
||||||
|
return &AuthHandler{
|
||||||
|
authService: authService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) SignUp(ctx context.Context, req *pb.SignUpRequest) (*pb.SignUpResponse, error) {
|
||||||
|
token, err := h.authService.SignUp(ctx, req.Email, req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("signup error: %v", err)
|
||||||
|
switch err {
|
||||||
|
case domain.ErrUserAlreadyExists:
|
||||||
|
return nil, status.Error(codes.AlreadyExists, "user already exists")
|
||||||
|
default:
|
||||||
|
return nil, status.Error(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.SignUpResponse{Token: token}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) SignIn(ctx context.Context, req *pb.SignInRequest) (*pb.SignInResponse, error) {
|
||||||
|
token, err := h.authService.SignIn(ctx, req.Email, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("signin error: %v", err)
|
||||||
|
switch err {
|
||||||
|
case domain.ErrInvalidPassword, domain.ErrUserNotFound:
|
||||||
|
return nil, status.Error(codes.Unauthenticated, "invalid credentials")
|
||||||
|
default:
|
||||||
|
return nil, status.Error(codes.Internal, "internal error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.SignInResponse{Token: token}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) ValidateToken(ctx context.Context, req *pb.ValidateTokenRequest) (*pb.ValidateTokenResponse, error) {
|
||||||
|
token := req.Token
|
||||||
|
|
||||||
|
// If token is empty, try to get it from Authorization header (for gRPC-Gateway)
|
||||||
|
if token == "" {
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if ok {
|
||||||
|
authHeaders := md.Get("authorization")
|
||||||
|
if len(authHeaders) > 0 {
|
||||||
|
parts := strings.Split(authHeaders[0], " ")
|
||||||
|
if len(parts) == 2 && parts[0] == "Bearer" {
|
||||||
|
token = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
return nil, status.Error(codes.Unauthenticated, "missing token")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.authService.ValidateToken(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("validate token error: %v", err)
|
||||||
|
return nil, status.Error(codes.Unauthenticated, "invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.ValidateTokenResponse{UserId: user.ID.String()}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorHandler struct{}
|
||||||
|
|
||||||
|
func (e *ErrorHandler) Handle(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"datarush/internal/auth/domain"
|
||||||
|
"datarush/internal/auth/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SignUpRequest struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignInRequest struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
authService *service.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
|
||||||
|
return &AuthHandler{
|
||||||
|
authService: authService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) SignUp(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req SignUpRequest
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("read body error: %v", err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
log.Printf("signup validation error: %v", err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Email == "" || req.Username == "" || req.Password == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := h.authService.SignUp(r.Context(), req.Email, req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("signup error: %v", err)
|
||||||
|
switch err {
|
||||||
|
case domain.ErrUserAlreadyExists:
|
||||||
|
w.WriteHeader(http.StatusConflict)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "user already exists"})
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "internal error"})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(TokenResponse{Token: token})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) SignIn(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req SignInRequest
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("read body error: %v", err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
log.Printf("signin validation error: %v", err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Email == "" || req.Password == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := h.authService.SignIn(r.Context(), req.Email, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("signin error: %v", err)
|
||||||
|
switch err {
|
||||||
|
case domain.ErrInvalidPassword, domain.ErrUserNotFound:
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "invalid credentials"})
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "internal error"})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(TokenResponse{Token: token})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token from Authorization header
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(authHeader, " ")
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := parts[1]
|
||||||
|
|
||||||
|
user, err := h.authService.ValidateToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("validate token error: %v", err)
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(UserResponse{
|
||||||
|
ID: user.ID.String(),
|
||||||
|
Email: user.Email,
|
||||||
|
Username: user.Username,
|
||||||
|
CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"datarush/internal/auth/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserAchievementResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
ReceivedAt string `json:"received_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserDetailResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Avatar *string `json:"avatar"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
Achievements []UserAchievementResponse `json:"achievements"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatResponse struct {
|
||||||
|
TotalAttempts int `json:"total_attempts"`
|
||||||
|
SolvedTasks int `json:"solved_tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserHandler struct {
|
||||||
|
authService *service.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserHandler(authService *service.AuthService) *UserHandler {
|
||||||
|
return &UserHandler{
|
||||||
|
authService: authService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) GetMe(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token from Authorization header
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(authHeader, " ")
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := parts[1]
|
||||||
|
|
||||||
|
user, err := h.authService.ValidateToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("validate token error: %v", err)
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(UserDetailResponse{
|
||||||
|
ID: user.ID.String(),
|
||||||
|
Email: user.Email,
|
||||||
|
Username: user.Username,
|
||||||
|
Avatar: nil,
|
||||||
|
CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
Achievements: []UserAchievementResponse{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := r.Header.Get("X-User-ID")
|
||||||
|
if userID == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "invalid user_id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract token from Authorization header for verification
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(authHeader, " ")
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := parts[1]
|
||||||
|
|
||||||
|
_, err := h.authService.ValidateToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("validate token error: %v", err)
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, return a simple user response
|
||||||
|
// In a real implementation, you would fetch from the database
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(UserDetailResponse{
|
||||||
|
ID: userID,
|
||||||
|
Email: "user@example.com",
|
||||||
|
Username: "user",
|
||||||
|
Avatar: nil,
|
||||||
|
CreatedAt: "2024-12-15T10:30:00Z",
|
||||||
|
Achievements: []UserAchievementResponse{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) GetMyStat(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(authHeader, " ")
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := parts[1]
|
||||||
|
|
||||||
|
_, err := h.authService.ValidateToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("validate token error: %v", err)
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Detail: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(StatResponse{
|
||||||
|
TotalAttempts: 0,
|
||||||
|
SolvedTasks: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) GetLeaderboard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return empty leaderboard for now
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode([]UserDetailResponse{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package interceptor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoggerInterceptor struct{}
|
||||||
|
|
||||||
|
func NewLoggerInterceptor() *LoggerInterceptor {
|
||||||
|
return &LoggerInterceptor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (li *LoggerInterceptor) UnaryServerInterceptor(
|
||||||
|
ctx context.Context,
|
||||||
|
req interface{},
|
||||||
|
info *grpc.UnaryServerInfo,
|
||||||
|
handler grpc.UnaryHandler,
|
||||||
|
) (interface{}, error) {
|
||||||
|
log.Printf("gRPC call: %s", info.FullMethod)
|
||||||
|
return handler(ctx, req)
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"datarush/internal/auth/domain"
|
||||||
|
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
usersTable = "users"
|
||||||
|
)
|
||||||
|
|
||||||
|
var psql = sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||||
|
|
||||||
|
type UserRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserRepository(db *sqlx.DB) *UserRepository {
|
||||||
|
return &UserRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) Create(ctx context.Context, user *domain.User) error {
|
||||||
|
now := time.Now()
|
||||||
|
user.CreatedAt = now
|
||||||
|
user.UpdatedAt = now
|
||||||
|
|
||||||
|
query := psql.Insert(usersTable).
|
||||||
|
Columns("id", "email", "username", "password", "created_at", "updated_at").
|
||||||
|
Values(user.ID.String(), user.Email, user.Username, user.Password, user.CreatedAt, user.UpdatedAt)
|
||||||
|
|
||||||
|
_, err := query.RunWith(r.db).ExecContext(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
query := psql.Select("id", "email", "username", "password", "created_at", "updated_at").
|
||||||
|
From(usersTable).
|
||||||
|
Where(sq.Eq{"email": email})
|
||||||
|
|
||||||
|
sqlQuery, args, err := query.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var user domain.User
|
||||||
|
err = r.db.GetContext(ctx, &user, sqlQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, domain.ErrUserNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) GetByID(ctx context.Context, id domain.ID) (*domain.User, error) {
|
||||||
|
query := psql.Select("id", "email", "username", "password", "created_at", "updated_at").
|
||||||
|
From(usersTable).
|
||||||
|
Where(sq.Eq{"id": id.String()})
|
||||||
|
|
||||||
|
sqlQuery, args, err := query.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var user domain.User
|
||||||
|
err = r.db.GetContext(ctx, &user, sqlQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, domain.ErrUserNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) Update(ctx context.Context, user *domain.User) error {
|
||||||
|
user.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
query := psql.Update(usersTable).
|
||||||
|
Set("email", user.Email).
|
||||||
|
Set("username", user.Username).
|
||||||
|
Set("password", user.Password).
|
||||||
|
Set("updated_at", user.UpdatedAt).
|
||||||
|
Where(sq.Eq{"id": user.ID.String()})
|
||||||
|
|
||||||
|
_, err := query.RunWith(r.db).ExecContext(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) Delete(ctx context.Context, id domain.ID) error {
|
||||||
|
query := psql.Delete(usersTable).
|
||||||
|
Where(sq.Eq{"id": id.String()})
|
||||||
|
|
||||||
|
_, err := query.RunWith(r.db).ExecContext(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"datarush/internal/auth/config"
|
||||||
|
grpcHandlers "datarush/internal/auth/handler/grpc"
|
||||||
|
httpHandlers "datarush/internal/auth/handler/http"
|
||||||
|
authPostgresRepo "datarush/internal/auth/repository/postgres"
|
||||||
|
"datarush/internal/auth/service"
|
||||||
|
|
||||||
|
pb "datarush/pkg/api/auth"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/reflection"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
httpReadTimeout = 10 * time.Second
|
||||||
|
httpWriteTimeout = 10 * time.Second
|
||||||
|
httpIdleTimeout = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
grpcServer *grpc.Server
|
||||||
|
config *config.Config
|
||||||
|
db *sqlx.DB
|
||||||
|
httpServer *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg *config.Config) *Server {
|
||||||
|
return &Server{
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
// Connect to database
|
||||||
|
db, err := sqlx.Connect("postgres", s.config.BuildPostgresConnStr())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to postgres: %w", err)
|
||||||
|
}
|
||||||
|
s.db = db
|
||||||
|
|
||||||
|
// Create tables
|
||||||
|
if err := s.createTables(); err != nil {
|
||||||
|
return fmt.Errorf("failed to create tables: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register gRPC services
|
||||||
|
if err := s.registerGRPCServices(); err != nil {
|
||||||
|
return fmt.Errorf("failed to register gRPC services: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start gRPC server in a goroutine
|
||||||
|
go func() {
|
||||||
|
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.config.GRPCPort))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to listen on grpc port: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("starting gRPC server on port %d", s.config.GRPCPort)
|
||||||
|
if err := s.grpcServer.Serve(lis); err != nil {
|
||||||
|
log.Fatalf("failed to serve gRPC: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start HTTP server
|
||||||
|
if err := s.startHTTPServer(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start HTTP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) registerGRPCServices() error {
|
||||||
|
s.grpcServer = grpc.NewServer()
|
||||||
|
|
||||||
|
// Create repositories
|
||||||
|
userRepo := authPostgresRepo.NewUserRepository(s.db)
|
||||||
|
|
||||||
|
// Create services
|
||||||
|
authService := service.NewAuthService(userRepo, s.config.JWTSecret)
|
||||||
|
|
||||||
|
// Create and register handlers
|
||||||
|
authHandler := grpcHandlers.NewAuthHandler(authService)
|
||||||
|
pb.RegisterAuthServiceServer(s.grpcServer, authHandler)
|
||||||
|
|
||||||
|
// Enable reflection if configured
|
||||||
|
if s.config.GRPCEnableReflection {
|
||||||
|
reflection.Register(s.grpcServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) startHTTPServer() error {
|
||||||
|
// Create repositories
|
||||||
|
userRepo := authPostgresRepo.NewUserRepository(s.db)
|
||||||
|
|
||||||
|
// Create services
|
||||||
|
authService := service.NewAuthService(userRepo, s.config.JWTSecret)
|
||||||
|
|
||||||
|
// Create HTTP handlers
|
||||||
|
authHandler := httpHandlers.NewAuthHandler(authService)
|
||||||
|
userHandler := httpHandlers.NewUserHandler(authService)
|
||||||
|
|
||||||
|
// Create router
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Register routes with middleware wrapper
|
||||||
|
mux.HandleFunc("/api/v1/sign-up", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authHandler.SignUp(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/api/v1/sign-in", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authHandler.SignIn(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/api/v1/me", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userHandler.GetMe(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/api/v1/users/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Extract user_id from path /api/v1/users/:user_id
|
||||||
|
userID := strings.TrimPrefix(r.URL.Path, "/api/v1/users/")
|
||||||
|
r.Header.Set("X-User-ID", userID)
|
||||||
|
userHandler.GetUser(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/api/v1/me/stat", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userHandler.GetMyStat(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/api/v1/leaderboard", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userHandler.GetLeaderboard(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.httpServer = &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", s.config.HTTPPort),
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: httpReadTimeout,
|
||||||
|
WriteTimeout: httpWriteTimeout,
|
||||||
|
IdleTimeout: httpIdleTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("starting HTTP server on port %d", s.config.HTTPPort)
|
||||||
|
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("failed to start HTTP server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Stop() {
|
||||||
|
log.Println("shutting down auth server...")
|
||||||
|
|
||||||
|
if s.httpServer != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := s.httpServer.Shutdown(ctx); err != nil {
|
||||||
|
log.Printf("failed to shutdown HTTP server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.grpcServer != nil {
|
||||||
|
s.grpcServer.GracefulStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.db != nil {
|
||||||
|
if err := s.db.Close(); err != nil {
|
||||||
|
log.Printf("failed to close database: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("auth server stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) createTables() error {
|
||||||
|
schema := `
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
username VARCHAR(255) NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := s.db.Exec(schema)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"datarush/internal/auth/domain"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tokenExpiration = 24 * time.Hour
|
||||||
|
bcryptCost = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRepository interface {
|
||||||
|
Create(ctx context.Context, user *domain.User) error
|
||||||
|
GetByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||||
|
GetByID(ctx context.Context, id domain.ID) (*domain.User, error)
|
||||||
|
Update(ctx context.Context, user *domain.User) error
|
||||||
|
Delete(ctx context.Context, id domain.ID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthService struct {
|
||||||
|
repo UserRepository
|
||||||
|
jwtSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthService(repo UserRepository, jwtSecret string) *AuthService {
|
||||||
|
return &AuthService{
|
||||||
|
repo: repo,
|
||||||
|
jwtSecret: jwtSecret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) SignUp(ctx context.Context, email, username, password string) (string, error) {
|
||||||
|
// Check if user already exists
|
||||||
|
_, err := s.repo.GetByEmail(ctx, email)
|
||||||
|
if err == nil {
|
||||||
|
return "", domain.ErrUserAlreadyExists
|
||||||
|
}
|
||||||
|
if err != domain.ErrUserNotFound {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
user := &domain.User{
|
||||||
|
ID: domain.NewID(),
|
||||||
|
Email: email,
|
||||||
|
Username: username,
|
||||||
|
Password: string(hashedPassword),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.Create(ctx, user); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate token
|
||||||
|
token, err := s.generateToken(user)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) SignIn(ctx context.Context, email, password string) (string, error) {
|
||||||
|
// Get user by email
|
||||||
|
user, err := s.repo.GetByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
if err == domain.ErrUserNotFound {
|
||||||
|
return "", domain.ErrInvalidPassword
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check password
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||||
|
return "", domain.ErrInvalidPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate token
|
||||||
|
token, err := s.generateToken(user)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) ValidateToken(ctx context.Context, tokenString string) (*domain.UserWithoutPassword, error) {
|
||||||
|
userID, err := s.parseToken(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.repo.GetByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.UserWithoutPassword{
|
||||||
|
ID: user.ID,
|
||||||
|
Email: user.Email,
|
||||||
|
Username: user.Username,
|
||||||
|
CreatedAt: user.CreatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) generateToken(user *domain.User) (string, error) {
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"user_id": user.ID.String(),
|
||||||
|
"email": user.Email,
|
||||||
|
"username": user.Username,
|
||||||
|
"exp": time.Now().Add(tokenExpiration).Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
tokenString, err := token.SignedString([]byte(s.jwtSecret))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) parseToken(tokenString string) (domain.ID, error) {
|
||||||
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte(s.jwtSecret), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return domain.ID{}, domain.ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
return domain.ID{}, domain.ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return domain.ID{}, domain.ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDStr, ok := claims["user_id"].(string)
|
||||||
|
if !ok {
|
||||||
|
return domain.ID{}, domain.ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := domain.ParseID(userIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return domain.ID{}, domain.ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return userID, nil
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ type Config struct {
|
|||||||
DBPassword string
|
DBPassword string
|
||||||
DBName string
|
DBName string
|
||||||
RedisURI string
|
RedisURI string
|
||||||
|
AuthGRPCAddr string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
@@ -39,6 +40,7 @@ func Load() (*Config, error) {
|
|||||||
DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"),
|
DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"),
|
||||||
DBName: getEnv("POSTGRES_DATABASE", "postgres"),
|
DBName: getEnv("POSTGRES_DATABASE", "postgres"),
|
||||||
RedisURI: getEnv("REDIS_URI", "redis://localhost:6379"),
|
RedisURI: getEnv("REDIS_URI", "redis://localhost:6379"),
|
||||||
|
AuthGRPCAddr: getEnv("AUTH_GRPC_ADDR", "localhost:50052"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
authPb "datarush/pkg/api/auth"
|
||||||
|
orderPb "datarush/pkg/api/order"
|
||||||
|
|
||||||
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
httpReadTimeout = 10 * time.Second
|
||||||
|
httpWriteTimeout = 10 * time.Second
|
||||||
|
httpIdleTimeout = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartGateway(grpcPort, httpPort int, authGrpcAddr string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
gwmux := runtime.NewServeMux()
|
||||||
|
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
|
||||||
|
|
||||||
|
// Register order service
|
||||||
|
grpcServerAddr := fmt.Sprintf("localhost:%d", grpcPort)
|
||||||
|
if err := orderPb.RegisterOrderServiceHandlerFromEndpoint(ctx, gwmux, grpcServerAddr, opts); err != nil {
|
||||||
|
return fmt.Errorf("failed to register order service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register auth service
|
||||||
|
if err := authPb.RegisterAuthServiceHandlerFromEndpoint(ctx, gwmux, authGrpcAddr, opts); err != nil {
|
||||||
|
return fmt.Errorf("failed to register auth service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", httpPort),
|
||||||
|
Handler: gwmux,
|
||||||
|
ReadTimeout: httpReadTimeout,
|
||||||
|
WriteTimeout: httpWriteTimeout,
|
||||||
|
IdleTimeout: httpIdleTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("starting gRPC-Gateway on port %d", httpPort)
|
||||||
|
return srv.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGRPCListener(port int) (net.Listener, error) {
|
||||||
|
return net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
orderPostgresRepo "datarush/internal/lms/repository/postgres"
|
orderPostgresRepo "datarush/internal/lms/repository/postgres"
|
||||||
"datarush/internal/lms/service"
|
"datarush/internal/lms/service"
|
||||||
|
|
||||||
|
authPb "datarush/pkg/api/auth"
|
||||||
pb "datarush/pkg/api/order"
|
pb "datarush/pkg/api/order"
|
||||||
|
|
||||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
@@ -72,6 +73,12 @@ func runHTTPHandler(s *Server, grpcServerEndpoint *string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register auth service
|
||||||
|
if err := registerAuthService(ctx, gwmux, s.config.AuthGRPCAddr, opts); err != nil {
|
||||||
|
log.Printf("failed to register auth service: %v", err)
|
||||||
|
// Don't fail completely if auth service is not available
|
||||||
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/healthz", httpHandlers.NewHealthHandler(s.db, s.redisDB))
|
mux.Handle("/healthz", httpHandlers.NewHealthHandler(s.db, s.redisDB))
|
||||||
mux.Handle("/", gwmux)
|
mux.Handle("/", gwmux)
|
||||||
@@ -87,6 +94,19 @@ func runHTTPHandler(s *Server, grpcServerEndpoint *string) error {
|
|||||||
return srv.ListenAndServe()
|
return srv.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerAuthService(ctx context.Context, gwmux *runtime.ServeMux, authAddr string, opts []grpc.DialOption) error {
|
||||||
|
if err := authPb.RegisterAuthServiceHandlerFromEndpoint(ctx, gwmux, authAddr, opts); err != nil {
|
||||||
|
return fmt.Errorf("register auth service handler: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("registered auth service from %s", authAddr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerAuthHandlerFromEndpoint(ctx context.Context, gwmux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error {
|
||||||
|
// We'll import the proto and register it here
|
||||||
|
// For now, this is a placeholder that will be called from gateway integration
|
||||||
|
return nil
|
||||||
|
|
||||||
func getDatabase(cfg config.Config) (*sqlx.DB, error) {
|
func getDatabase(cfg config.Config) (*sqlx.DB, error) {
|
||||||
db, err := sqlx.Connect("postgres", cfg.BuildPostgresConnStr())
|
db, err := sqlx.Connect("postgres", cfg.BuildPostgresConnStr())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user