package repositoryservices import ( "context" "database/sql" "errors" "fmt" "strings" "git.jamestombleson.com/jtom38/newsbot-api/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity" "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) const ( ErrPasswordNotLongEnough = "password needs to be 8 character or longer" ErrPasswordMissingSpecialCharacter = "password needs to contain one of the following: !, @, #" ErrInvalidPassword = "invalid password" ) type UserServices interface { DoesUserExist(ctx context.Context, username string) error DoesPasswordMatchHash(ctx context.Context, username, password string) error GetUser(ctx context.Context, username string) (entity.UserEntity, error) AddScopes(ctx context.Context, username string, scopes []string) error RemoveScopes(ctx context.Context, username string, scopes []string) error Create(ctx context.Context, name, password, scope string) (entity.UserEntity, error) NewSessionToken(ctx context.Context, name string) (string, error) CheckPasswordForRequirements(password string) error } // This will handle operations that are user related, but one layer higher then the repository type UserService struct { repo repository.Users } // This is a layer on top of the Users Repository. // Use this over directly talking to the table when ever possible. func NewUserService(conn *sql.DB) UserService { return UserService{ repo: repository.NewUserRepository(conn), } } func (us UserService) DoesUserExist(ctx context.Context, username string) error { _, err := us.repo.GetByName(ctx, username) if err != nil { return err } return nil } func (us UserService) DoesPasswordMatchHash(ctx context.Context, username, password string) error { model, err := us.GetUser(ctx, username) if err != nil { return err } err = bcrypt.CompareHashAndPassword([]byte(model.Hash), []byte(password)) if err != nil { return errors.New(ErrInvalidPassword) } return nil } func (us UserService) GetUser(ctx context.Context, username string) (entity.UserEntity, error) { return us.repo.GetByName(ctx, username) } func (us UserService) AddScopes(ctx context.Context, username string, scopes []string) error { usr, err := us.repo.GetByName(ctx, username) if err != nil { return err } if usr.Username != username { return errors.New(repository.ErrUserNotFound) } currentScopes := strings.Split(usr.Scopes, ",") // check the current scopes for _, item := range scopes { if !strings.Contains(usr.Scopes, item) { currentScopes = append(currentScopes, item) } } return us.repo.UpdateScopes(ctx, username, strings.Join(currentScopes, ",")) } func (us UserService) RemoveScopes(ctx context.Context, username string, scopes []string) error { usr, err := us.repo.GetByName(ctx, username) if err != nil { return err } if usr.Username != username { return errors.New(repository.ErrUserNotFound) } var newScopes []string // check all the scopes that are currently assigned for _, item := range strings.Split(usr.Scopes, ",") { // check the scopes given, if one matches skip it if us.doesScopeExist(scopes, item) { continue } // did not match, add it newScopes = append(newScopes, item) } return us.repo.UpdateScopes(ctx, username, strings.Join(newScopes, ",")) } func (us UserService) doesScopeExist(scopes []string, target string) bool { for _, item := range scopes { if item == target { return true } } return false } func (us UserService) Create(ctx context.Context, name, password, scope string) (entity.UserEntity, error) { err := us.CheckPasswordForRequirements(password) if err != nil { return entity.UserEntity{}, err } token, err := uuid.NewV7() if err != nil { return entity.UserEntity{}, err } us.repo.Create(ctx, name, password, token.String(), domain.ScopeArticleRead) return entity.UserEntity{}, nil } func (us UserService) NewSessionToken(ctx context.Context, name string) (string, error) { token, err := uuid.NewV7() if err != nil { return "", err } rows, err := us.repo.UpdateSessionToken(ctx, name, token.String()) if err != nil { return "", err } if rows != 1 { return "", fmt.Errorf("UserService.NewSessionToken %w", err) } return token.String(), nil } func (us UserService) CheckPasswordForRequirements(password string) error { err := us.checkPasswordLength(password) if err != nil { return err } err = us.checkPasswordForSpecialCharacters(password) if err != nil { return err } return nil } func (us UserService) checkPasswordLength(password string) error { if len(password) < 8 { return errors.New(ErrPasswordNotLongEnough) } return nil } func (us UserService) checkPasswordForSpecialCharacters(password string) error { var chars []string chars = append(chars, "!") chars = append(chars, "@") chars = append(chars, "#") for _, char := range chars { if strings.Contains(password, char) { return nil } } return errors.New(ErrPasswordMissingSpecialCharacter) }