I am batman

This commit is contained in:
2026-06-20 11:07:01 -04:00
commit 013d01358c
52 changed files with 7615 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
package database
import (
"os"
"go-microservices/services/auth-service/internal/models"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func Init() (*gorm.DB, error) {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
dsn = "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable TimeZone=UTC"
}
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
if err := db.AutoMigrate(&models.Auth{}, &models.Test{}); err != nil {
return nil, err
}
return db, nil
}

View File

@@ -0,0 +1,23 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Auth struct {
gorm.Model
Username string `gorm:"uniqueIndex;not null"`
Email string `gorm:"uniqueIndex;not null"`
Password string `gorm:"not null"`
VerificationToken string `json:"-"`
ResetToken string `json:"-"`
ResetTokenExpiry time.Time `json:"-"`
Role string `gorm:"type:varchar(50);default:user" json:"role,omitempty"`
}
type Test struct {
gorm.Model
Content string `gorm:"type:text;not null"`
}

View File

@@ -0,0 +1,63 @@
package repository
import (
"go-microservices/services/auth-service/internal/models"
"gorm.io/gorm"
)
type Repository struct {
DB *gorm.DB
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{DB: db}
}
func (r *Repository) CreateAuth(a *models.Auth) error {
return r.DB.Create(a).Error
}
func (r *Repository) GetAuthByEmail(email string) (*models.Auth, error) {
var a models.Auth
if err := r.DB.Where("email = ?", email).First(&a).Error; err != nil {
return nil, err
}
return &a, nil
}
func (r *Repository) GetAuthByID(id uint) (*models.Auth, error) {
var a models.Auth
if err := r.DB.First(&a, id).Error; err != nil {
return nil, err
}
return &a, nil
}
func (r *Repository) CreateTest(t *models.Test) error {
return r.DB.Create(t).Error
}
func (r *Repository) ListTests() ([]models.Test, error) {
var tests []models.Test
if err := r.DB.Order("id desc").Find(&tests).Error; err != nil {
return nil, err
}
return tests, nil
}
func (r *Repository) GetTestByID(id uint) (*models.Test, error) {
var t models.Test
if err := r.DB.First(&t, id).Error; err != nil {
return nil, err
}
return &t, nil
}
func (r *Repository) UpdateTest(t *models.Test) error {
return r.DB.Save(t).Error
}
func (r *Repository) DeleteTest(id uint) error {
return r.DB.Delete(&models.Test{}, id).Error
}

View File

@@ -0,0 +1,210 @@
package server
import (
"context"
"fmt"
"strconv"
"time"
pb "go-microservices/proto/auth"
"go-microservices/services/auth-service/internal/models"
"go-microservices/services/auth-service/internal/repository"
"go-microservices/services/auth-service/internal/utils"
"golang.org/x/crypto/bcrypt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type AuthServer struct {
pb.UnimplementedAuthServiceServer
repo *repository.Repository
}
func NewAuthServer(repo *repository.Repository) *AuthServer {
return &AuthServer{repo: repo}
}
func (s *AuthServer) SignUp(ctx context.Context, req *pb.SignUpRequest) (*pb.SignUpResponse, error) {
hashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to hash password: %v", err)
}
auth := &models.Auth{
Username: req.Username,
Email: req.Email,
Password: string(hashed),
Role: "user", // default role (proto SignUpRequest doesn't include role)
}
if err := s.repo.CreateAuth(auth); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create user: %v", err)
}
userID := fmt.Sprintf("%d", auth.ID)
return &pb.SignUpResponse{UserId: userID, Message: "registered"}, nil
}
func (s *AuthServer) SignIn(ctx context.Context, req *pb.SignInRequest) (*pb.SignInResponse, error) {
auth, err := s.repo.GetAuthByEmail(req.Email)
if err != nil || auth == nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid credentials")
}
if err := bcrypt.CompareHashAndPassword([]byte(auth.Password), []byte(req.Password)); err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid credentials")
}
accessToken, refreshToken, err := utils.GenerateJWT(*auth)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate tokens: %v", err)
}
userID := fmt.Sprintf("%d", auth.ID)
return &pb.SignInResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
UserId: userID,
Message: "logged in",
}, nil
}
func (s *AuthServer) ValidateToken(ctx context.Context, req *pb.ValidateTokenRequest) (*pb.ValidateTokenResponse, error) {
claims, err := utils.ValidateJWT(req.Token, false)
if err != nil {
return &pb.ValidateTokenResponse{Valid: false, UserId: "", Message: "invalid token"}, nil
}
var userID string
if sub, ok := claims["sub"]; ok {
switch v := sub.(type) {
case string:
userID = v
case float64:
userID = strconv.FormatFloat(v, 'f', 0, 64)
default:
userID = fmt.Sprintf("%v", v)
}
}
if userID == "" {
return &pb.ValidateTokenResponse{Valid: false, UserId: "", Message: "invalid token"}, nil
}
return &pb.ValidateTokenResponse{Valid: true, UserId: userID, Message: "valid"}, nil
}
func (s *AuthServer) GetUserInfo(ctx context.Context, req *pb.GetUserInfoRequest) (*pb.GetUserInfoResponse, error) {
// expect req.UserId (proto field user_id)
if req.UserId == "" {
return nil, status.Errorf(codes.InvalidArgument, "user id required")
}
u64, err := strconv.ParseUint(req.UserId, 10, 64)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user id: %v", err)
}
user, err := s.repo.GetAuthByID(uint(u64))
if err != nil || user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
return &pb.GetUserInfoResponse{
UserId: fmt.Sprintf("%d", user.ID),
Username: user.Username,
Email: user.Email,
Roles: []string{user.Role},
}, nil
}
// CreateTest creates a simple Test record in the database
func (s *AuthServer) CreateTest(ctx context.Context, req *pb.CreateTestRequest) (*pb.CreateTestResponse, error) {
if req.Content == "" {
return nil, status.Errorf(codes.InvalidArgument, "content required")
}
t := &models.Test{Content: req.Content}
if err := s.repo.CreateTest(t); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create test: %v", err)
}
return &pb.CreateTestResponse{
Test: &pb.Test{
Id: uint64(t.ID),
Content: t.Content,
CreatedAt: t.CreatedAt.Format(time.RFC3339),
},
}, nil
}
// ListTests returns recent test records
func (s *AuthServer) ListTests(ctx context.Context, req *pb.ListTestsRequest) (*pb.ListTestsResponse, error) {
tests, err := s.repo.ListTests()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list tests: %v", err)
}
resp := &pb.ListTestsResponse{Tests: make([]*pb.Test, 0, len(tests))}
for _, t := range tests {
resp.Tests = append(resp.Tests, &pb.Test{
Id: uint64(t.ID),
Content: t.Content,
CreatedAt: t.CreatedAt.Format(time.RFC3339),
})
}
return resp, nil
}
// GetTest retrieves a single test by ID
func (s *AuthServer) GetTest(ctx context.Context, req *pb.GetTestRequest) (*pb.GetTestResponse, error) {
if req.Id == 0 {
return nil, status.Errorf(codes.InvalidArgument, "test id required")
}
t, err := s.repo.GetTestByID(uint(req.Id))
if err != nil || t == nil {
return nil, status.Errorf(codes.NotFound, "test not found")
}
return &pb.GetTestResponse{
Test: &pb.Test{
Id: uint64(t.ID),
Content: t.Content,
CreatedAt: t.CreatedAt.Format(time.RFC3339),
},
}, nil
}
// UpdateTest updates an existing test
func (s *AuthServer) UpdateTest(ctx context.Context, req *pb.UpdateTestRequest) (*pb.UpdateTestResponse, error) {
if req.Id == 0 {
return nil, status.Errorf(codes.InvalidArgument, "test id required")
}
if req.Content == "" {
return nil, status.Errorf(codes.InvalidArgument, "content required")
}
t, err := s.repo.GetTestByID(uint(req.Id))
if err != nil || t == nil {
return nil, status.Errorf(codes.NotFound, "test not found")
}
t.Content = req.Content
if err := s.repo.UpdateTest(t); err != nil {
return nil, status.Errorf(codes.Internal, "failed to update test: %v", err)
}
return &pb.UpdateTestResponse{
Test: &pb.Test{
Id: uint64(t.ID),
Content: t.Content,
CreatedAt: t.CreatedAt.Format(time.RFC3339),
},
}, nil
}
// DeleteTest deletes a test by ID
func (s *AuthServer) DeleteTest(ctx context.Context, req *pb.DeleteTestRequest) (*pb.DeleteTestResponse, error) {
if req.Id == 0 {
return nil, status.Errorf(codes.InvalidArgument, "test id required")
}
if err := s.repo.DeleteTest(uint(req.Id)); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete test: %v", err)
}
return &pb.DeleteTestResponse{
Success: true,
Message: "test deleted",
}, nil
}

View File

@@ -0,0 +1,101 @@
package utils
import (
"crypto/rand"
"encoding/hex"
"errors"
"log"
"os"
"time"
"go-microservices/services/auth-service/internal/models"
"github.com/golang-jwt/jwt/v4"
)
var (
accessTokenSecret []byte
refreshTokenSecret []byte
)
func init() {
access := os.Getenv("JWT_ACCESS_SECRET")
if access == "" {
access = os.Getenv("JWT_SECRET")
}
refresh := os.Getenv("JWT_REFRESH_SECRET")
if refresh == "" {
refresh = os.Getenv("JWT_SECRET")
}
accessTokenSecret = []byte(access)
refreshTokenSecret = []byte(refresh)
}
// GenerateJWT generates an access token and refresh token for the provided user.
// The function expects the provided models.Auth (or models.User) to have ID, Email and Role fields.
func GenerateJWT(user models.Auth) (string, string, error) {
accessClaims := jwt.MapClaims{
"sub": user.ID,
"email": user.Email,
"role": user.Role,
"exp": time.Now().Add(15 * time.Minute).Unix(),
"iat": time.Now().Unix(),
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
signedAccessToken, err := accessToken.SignedString(accessTokenSecret)
if err != nil {
log.Println("Error generating access token:", err)
return "", "", err
}
refreshClaims := jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
"iat": time.Now().Unix(),
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
signedRefreshToken, err := refreshToken.SignedString(refreshTokenSecret)
if err != nil {
log.Println("Error generating refresh token:", err)
return "", "", err
}
return signedAccessToken, signedRefreshToken, nil
}
// ValidateJWT parses and validates the provided token string.
// If isRefreshToken is true, the refresh secret is used; otherwise the access secret is used.
func ValidateJWT(tokenString string, isRefreshToken bool) (jwt.MapClaims, error) {
secret := accessTokenSecret
if isRefreshToken {
secret = refreshTokenSecret
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.NewValidationError("invalid signing method", jwt.ValidationErrorSignatureInvalid)
}
return secret, nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
// GenerateRandomToken returns a 128-bit random hex token (32 chars).
func GenerateRandomToken() string {
bytes := make([]byte, 16)
_, err := rand.Read(bytes)
if err != nil {
panic("Failed to generate random token")
}
return hex.EncodeToString(bytes)
}