This commit is contained in:
2024-10-13 13:31:58 +04:00
commit aec8d7ed48
54 changed files with 2827 additions and 0 deletions

View File

@ -0,0 +1,16 @@
package domain
import "errors"
var (
ErrInternal = errors.New("internal error")
ErrConflictingData = errors.New("data conflicts with existing data in unique column")
ErrDataNotFound = errors.New("data not found")
ErrInvalidEmailCredentials = errors.New("invalid email or password")
ErrInvalidUsernameCredentials = errors.New("invalid username or password")
ErrTokenCreation = errors.New("error creating token")
ErrExpiredToken = errors.New("access token has expired")
ErrUsernameExists = errors.New("username already exists")
ErrEmailExists = errors.New("email already exists")
ErrInvalidToken = errors.New("invalid token")
)

View File

@ -0,0 +1,20 @@
package domain
import (
"time"
"github.com/google/uuid"
)
type Message struct {
UserID uuid.UUID
ChatID string
Content string
Type string
Timestamp time.Time
}
type StreamMessage struct {
*Message
Commit func() error
}

View File

@ -0,0 +1,7 @@
package domain
import "github.com/google/uuid"
type AuthPayload struct {
UserID uuid.UUID
}

View File

@ -0,0 +1,14 @@
package domain
import (
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID
Username string
Email string
Password string
// CreatedAt time.Time
// UpdatedAt time.Time
}

View File

@ -0,0 +1,17 @@
package port
import (
"context"
"github.com/aykhans/oh-my-chat/internal/core/domain"
)
type TokenService interface {
CreateToken(user *domain.User) (string, error)
VerifyToken(token string) (*domain.AuthPayload, error)
}
type AuthService interface {
LoginByEmail(ctx context.Context, email, password string) (string, error)
LoginByUsername(ctx context.Context, username, password string) (string, error)
}

View File

@ -0,0 +1,26 @@
package port
import (
"context"
"github.com/aykhans/oh-my-chat/internal/core/domain"
"github.com/google/uuid"
)
type MessageProducer interface {
ProduceMessage(ctx context.Context, message *domain.Message) error
}
type MessageConsumer interface {
ConsumeMessage(ctx context.Context, uid string, getChats func() []string, message chan<- *domain.StreamMessage) error
}
type MessageRepository interface {
CreateMessage(ctx context.Context, message *domain.Message) (*domain.Message, error)
}
type MessageService interface {
SendMessage(ctx context.Context, message *domain.Message) error
ReceiveMessage(ctx context.Context, userID uuid.UUID, message chan<- *domain.StreamMessage) error
CreateMessage(ctx context.Context, message *domain.Message) (*domain.Message, error)
}

View File

@ -0,0 +1,22 @@
package port
import (
"context"
"github.com/aykhans/oh-my-chat/internal/core/domain"
)
type UserRepository interface {
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
GetUserByUsername(ctx context.Context, username string) (*domain.User, error)
IsUsernameExists(ctx context.Context, username string) (bool, error)
IsEmailExists(ctx context.Context, email string) (bool, error)
// GetUserByID(ctx context.Context, id uint64) (*domain.User, error)
// DeleteUser(ctx context.Context, id uint64) error
}
type UserService interface {
Register(ctx context.Context, user *domain.User) (*domain.User, error)
// GetUser(ctx context.Context, id uint64) (*domain.User, error)
// DeleteUser(ctx context.Context, id uint64) error
}

View File

@ -0,0 +1,72 @@
package service
import (
"context"
"github.com/aykhans/oh-my-chat/internal/core/domain"
"github.com/aykhans/oh-my-chat/internal/core/port"
"github.com/aykhans/oh-my-chat/internal/core/utils"
)
type AuthService struct {
userRepository port.UserRepository
tokenService port.TokenService
}
// NewAuthService creates a new auth service instance
func NewAuthService(userRepository port.UserRepository, tokenService port.TokenService) *AuthService {
return &AuthService{
userRepository,
tokenService,
}
}
func (authService *AuthService) LoginByEmail(
ctx context.Context,
email, password string,
) (string, error) {
user, err := authService.userRepository.GetUserByEmail(ctx, email)
if err != nil {
if err == domain.ErrDataNotFound {
return "", domain.ErrInvalidEmailCredentials
}
return "", domain.ErrInternal
}
err = utils.ComparePassword(password, user.Password)
if err != nil {
return "", domain.ErrInvalidEmailCredentials
}
accessToken, err := authService.tokenService.CreateToken(user)
if err != nil {
return "", domain.ErrTokenCreation
}
return accessToken, nil
}
func (authService *AuthService) LoginByUsername(
ctx context.Context,
username, password string,
) (string, error) {
user, err := authService.userRepository.GetUserByEmail(ctx, username)
if err != nil {
if err == domain.ErrDataNotFound {
return "", domain.ErrInvalidUsernameCredentials
}
return "", domain.ErrInternal
}
err = utils.ComparePassword(password, user.Password)
if err != nil {
return "", domain.ErrInvalidUsernameCredentials
}
accessToken, err := authService.tokenService.CreateToken(user)
if err != nil {
return "", domain.ErrTokenCreation
}
return accessToken, nil
}

View File

@ -0,0 +1,55 @@
package service
import (
"context"
"github.com/aykhans/oh-my-chat/internal/core/domain"
"github.com/aykhans/oh-my-chat/internal/core/port"
"github.com/google/uuid"
)
type MessageService struct {
producer port.MessageProducer
consumer port.MessageConsumer
repo port.MessageRepository
}
func NewMessageService(
producerService port.MessageProducer,
consumerService port.MessageConsumer,
repo port.MessageRepository,
) *MessageService {
return &MessageService{
producerService,
consumerService,
repo,
}
}
func (chatServie *MessageService) SendMessage(
ctx context.Context,
message *domain.Message,
) error {
message.ChatID = "chat_" + message.ChatID
return chatServie.producer.ProduceMessage(ctx, message)
}
func (chatServie *MessageService) ReceiveMessage(
ctx context.Context,
userID uuid.UUID,
message chan<- *domain.StreamMessage,
) error {
return chatServie.consumer.ConsumeMessage(
ctx,
userID.String(),
func() []string { return []string{"chat_1", "chat_5", "chat_9"} },
message,
)
}
func (chatServie *MessageService) CreateMessage(
ctx context.Context,
message *domain.Message,
) (*domain.Message, error) {
return chatServie.repo.CreateMessage(ctx, message)
}

View File

@ -0,0 +1,49 @@
package service
import (
"context"
"github.com/aykhans/oh-my-chat/internal/core/domain"
"github.com/aykhans/oh-my-chat/internal/core/port"
"github.com/aykhans/oh-my-chat/internal/core/utils"
)
type UserService struct {
repo port.UserRepository
}
func NewUserService(repo port.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (userService *UserService) Register(
ctx context.Context,
user *domain.User,
) (*domain.User, error) {
if exists, err := userService.repo.IsUsernameExists(ctx, user.Username); err != nil {
return nil, domain.ErrInternal
} else if exists {
return nil, domain.ErrUsernameExists
}
if exists, err := userService.repo.IsEmailExists(ctx, user.Email); err != nil {
return nil, domain.ErrInternal
} else if exists {
return nil, domain.ErrEmailExists
}
hashedPassword, err := utils.HashPassword(user.Password)
if err != nil {
return nil, domain.ErrInternal
}
user.Password = hashedPassword
user, err = userService.repo.CreateUser(ctx, user)
if err != nil {
if err == domain.ErrConflictingData {
return nil, err
}
return nil, domain.ErrInternal
}
return user, nil
}

View File

@ -0,0 +1,9 @@
package utils
import (
"strings"
)
func Str2StrSlice(value string) []string {
return strings.Split(strings.ReplaceAll(value, " ", ""), ",")
}

15
internal/core/utils/os.go Normal file
View File

@ -0,0 +1,15 @@
package utils
import "os"
func ExitErr() {
os.Exit(1)
}
func GetEnvOrDefault(key, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}

View File

@ -0,0 +1,26 @@
package utils
import (
"golang.org/x/crypto/bcrypt"
)
// HashPassword hashes password and returns hashed password or error
func HashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword(
[]byte(password),
bcrypt.DefaultCost,
)
if err != nil {
return "", err
}
return string(hashedPassword), nil
}
// ComparePassword compares password with hashed password and returns error if they don't match or nil if they do
func ComparePassword(password, hashedPassword string) error {
return bcrypt.CompareHashAndPassword(
[]byte(hashedPassword),
[]byte(password),
)
}

View File

@ -0,0 +1,7 @@
package utils
import "time"
func GetNow() time.Time {
return time.Now()
}