feat(epic2): Implement core authentication and authorization services
- Implement Audit Service (2.5) - gRPC server with Record and Query operations - Database persistence with audit schema - Service registry integration - Entry point: cmd/audit-service - Implement Identity Service (2.2) - User CRUD operations - Password hashing with argon2id - Email verification and password reset flows - Entry point: cmd/identity-service - Fix package naming conflicts in user_service.go - Implement Auth Service (2.1) - JWT token generation and validation - Login, RefreshToken, ValidateToken, Logout RPCs - Integration with Identity Service - Entry point: cmd/auth-service - Note: RefreshToken entity needs Ent generation - Implement Authz Service (2.3, 2.4) - Permission checking and authorization - User roles and permissions retrieval - RBAC-based authorization - Entry point: cmd/authz-service - Implement gRPC clients for all services - Auth, Identity, Authz, and Audit clients - Service discovery integration - Full gRPC communication - Add service configurations to config/default.yaml - Create SUMMARY.md with implementation details and testing instructions - Fix compilation errors in Identity Service (password package conflicts) - All services build successfully and tests pass
This commit is contained in:
@@ -5,70 +5,132 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
authv1 "git.dcentral.systems/toolz/goplt/api/proto/generated/auth/v1"
|
||||
"git.dcentral.systems/toolz/goplt/pkg/registry"
|
||||
"git.dcentral.systems/toolz/goplt/pkg/services"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
// AuthClient implements AuthServiceClient using gRPC.
|
||||
// This is a stub implementation - will be fully implemented when proto files are generated in Phase 4.
|
||||
type AuthClient struct {
|
||||
registry registry.ServiceRegistry
|
||||
// conn will be set when proto files are available
|
||||
// conn *grpc.ClientConn
|
||||
conn *grpc.ClientConn
|
||||
client authv1.AuthServiceClient
|
||||
}
|
||||
|
||||
// NewAuthClient creates a new gRPC client for the Auth Service.
|
||||
func NewAuthClient(reg registry.ServiceRegistry) (services.AuthServiceClient, error) {
|
||||
return &AuthClient{
|
||||
client := &AuthClient{
|
||||
registry: reg,
|
||||
}, nil
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// connect connects to the Auth Service.
|
||||
func (c *AuthClient) connect(ctx context.Context) error {
|
||||
if c.conn != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
instances, err := c.registry.Discover(ctx, "auth-service")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to discover auth service: %w", err)
|
||||
}
|
||||
|
||||
if len(instances) == 0 {
|
||||
return fmt.Errorf("no instances found for auth-service")
|
||||
}
|
||||
|
||||
instance := instances[0]
|
||||
address := fmt.Sprintf("%s:%d", instance.Address, instance.Port)
|
||||
|
||||
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to auth-service at %s: %w", address, err)
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
c.client = authv1.NewAuthServiceClient(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Login authenticates a user and returns access and refresh tokens.
|
||||
func (c *AuthClient) Login(ctx context.Context, email, password string) (*services.TokenResponse, error) {
|
||||
// TODO: Implement when proto files are generated
|
||||
return nil, fmt.Errorf("not implemented: proto files not yet generated")
|
||||
if err := c.connect(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.Login(ctx, &authv1.LoginRequest{
|
||||
Email: email,
|
||||
Password: password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
return &services.TokenResponse{
|
||||
AccessToken: resp.AccessToken,
|
||||
RefreshToken: resp.RefreshToken,
|
||||
ExpiresIn: resp.ExpiresIn,
|
||||
TokenType: resp.TokenType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshToken refreshes an access token using a refresh token.
|
||||
func (c *AuthClient) RefreshToken(ctx context.Context, refreshToken string) (*services.TokenResponse, error) {
|
||||
// TODO: Implement when proto files are generated
|
||||
return nil, fmt.Errorf("not implemented: proto files not yet generated")
|
||||
if err := c.connect(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.RefreshToken(ctx, &authv1.RefreshTokenRequest{
|
||||
RefreshToken: refreshToken,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("refresh token failed: %w", err)
|
||||
}
|
||||
|
||||
return &services.TokenResponse{
|
||||
AccessToken: resp.AccessToken,
|
||||
RefreshToken: resp.RefreshToken,
|
||||
ExpiresIn: resp.ExpiresIn,
|
||||
TokenType: resp.TokenType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the token claims.
|
||||
func (c *AuthClient) ValidateToken(ctx context.Context, token string) (*services.TokenClaims, error) {
|
||||
// TODO: Implement when proto files are generated
|
||||
return nil, fmt.Errorf("not implemented: proto files not yet generated")
|
||||
if err := c.connect(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.ValidateToken(ctx, &authv1.ValidateTokenRequest{
|
||||
Token: token,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validate token failed: %w", err)
|
||||
}
|
||||
|
||||
return &services.TokenClaims{
|
||||
UserID: resp.UserId,
|
||||
Email: resp.Email,
|
||||
Roles: resp.Roles,
|
||||
ExpiresAt: resp.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Logout invalidates a refresh token.
|
||||
func (c *AuthClient) Logout(ctx context.Context, refreshToken string) error {
|
||||
// TODO: Implement when proto files are generated
|
||||
return fmt.Errorf("not implemented: proto files not yet generated")
|
||||
}
|
||||
if err := c.connect(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: connectToService will be implemented when proto files are generated
|
||||
// This function will discover and connect to a service instance via gRPC.
|
||||
// func connectToService(ctx context.Context, reg registry.ServiceRegistry, serviceName string) (*grpc.ClientConn, error) {
|
||||
// instances, err := reg.Discover(ctx, serviceName)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("failed to discover service %s: %w", serviceName, err)
|
||||
// }
|
||||
//
|
||||
// if len(instances) == 0 {
|
||||
// return nil, fmt.Errorf("no instances found for service %s", serviceName)
|
||||
// }
|
||||
//
|
||||
// // Use the first healthy instance (load balancing can be added later)
|
||||
// instance := instances[0]
|
||||
// address := fmt.Sprintf("%s:%d", instance.Address, instance.Port)
|
||||
//
|
||||
// // Create gRPC connection
|
||||
// conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("failed to connect to %s at %s: %w", serviceName, address, err)
|
||||
// }
|
||||
//
|
||||
// return conn, nil
|
||||
// }
|
||||
_, err := c.client.Logout(ctx, &authv1.LogoutRequest{
|
||||
RefreshToken: refreshToken,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("logout failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user