This commit is contained in:
Timur Kh.
2025-12-15 22:28:01 +03:00
parent 4a17c75d6f
commit f7a7b19275
25 changed files with 1383 additions and 3 deletions
+76
View File
@@ -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)
}
+11
View File
@@ -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")
)
+13
View File
@@ -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)
}
+19
View File
@@ -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
}
+86
View File
@@ -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
}
+13
View File
@@ -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)
}
}
+185
View File
@@ -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"),
})
}
+186
View File
@@ -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{})
}
+24
View File
@@ -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)
}
+106
View File
@@ -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
}
+229
View File
@@ -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
}
+164
View File
@@ -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
}