diff --git a/.gitignore b/.gitignore index 23aa3e3..f70865e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ go.work # Environment variables .env + +# Config +config.json diff --git a/application/dtos/login_dto.go b/application/dtos/login_dto.go new file mode 100644 index 0000000..2142e89 --- /dev/null +++ b/application/dtos/login_dto.go @@ -0,0 +1,28 @@ +package dtos + +import ( + "regexp" + "strings" +) + +type LoginDTO struct { + Email string + Password string + Token *string +} + +// Validator +func (dto *LoginDTO) Validate() bool { + rfc2822EmailPattern := `[a-z0-9!#$%&'*+/=?^_{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?` + trimmedEmail := strings.TrimSpace((*dto).Email) + trimmedPassword := strings.TrimSpace((*dto).Password) + + isOk, _ := regexp.MatchString(rfc2822EmailPattern, trimmedEmail) + + // TODO: check password for certain special characters + if !isOk || len(trimmedPassword) < 8 || len(trimmedPassword) > 16 { + return false + } + + return true +} diff --git a/application/dtos/login_validator_test.go b/application/dtos/login_validator_test.go new file mode 100644 index 0000000..364d677 --- /dev/null +++ b/application/dtos/login_validator_test.go @@ -0,0 +1,117 @@ +package dtos + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoginValidateShouldReturnFalseIfEmailIsNil(t *testing.T) { + // Arrange + loginDTO := LoginDTO{ + Password: "aaaaaaaa", + } + + // Act + result := loginDTO.Validate() + + // Assert + assert.False(t, result) +} + +func TestLoginValidateShouldReturnFalseIfEmailIsEmpty(t *testing.T) { + // Arrange + loginDTO := LoginDTO{ + Email: "", + Password: "aaaaaaaa", + } + + // Act + result := loginDTO.Validate() + + // Assert + assert.False(t, result) +} + +func TestLoginValidateShouldReturnFalseIfEmailIsMalformed(t *testing.T) { + // Arrange + loginDTO := LoginDTO{ + Email: "a@.com", + Password: "aaaaaaaa", + } + + // Act + result := loginDTO.Validate() + + // Assert + assert.False(t, result) +} + +func TestLoginValidateShouldReturnFalseIfPasswordIsNil(t *testing.T) { + // Arrange + loginDTO := LoginDTO{ + Email: "a@a.com", + } + + // Act + result := loginDTO.Validate() + + // Assert + assert.False(t, result) +} + +func TestLoginValidateShouldReturnFalseIfPasswordIsEmpty(t *testing.T) { + // Arrange + loginDTO := LoginDTO{ + Email: "a@a.com", + Password: "", + } + + // Act + result := loginDTO.Validate() + + // Assert + assert.False(t, result) +} + +func TestLoginValidateShouldReturnFalseIfPasswordIsShorterThanEightCharacters(t *testing.T) { + // Arrange + loginDTO := LoginDTO{ + Email: "a@a.com", + Password: "aaaa", + } + + // Act + result := loginDTO.Validate() + + // Assert + assert.False(t, result) +} + +func TestLoginValidateShouldReturnFalseIfPasswordIsLongerThanSixteenCharacters(t *testing.T) { + // Arrange + loginDTO := LoginDTO{ + Email: "a@a.com", + Password: "aaaaaaaaaaaaaaaaa", + } + + // Act + result := loginDTO.Validate() + + // Assert + assert.False(t, result) +} + +func TestLoginValidateShouldReturnTrueIfAllRequiredValuesArePresent(t *testing.T) { + // Arrange + loginDTO := LoginDTO{ + Email: "a@a.com", + Password: "aaaaaaaaaaa", + } + + // Act + result := loginDTO.Validate() + + // Assert + assert.True(t, result) +} diff --git a/application/dtos/user_dto.go b/application/dtos/user_dto.go index 911807a..ff8e630 100644 --- a/application/dtos/user_dto.go +++ b/application/dtos/user_dto.go @@ -37,7 +37,7 @@ func (dto *UserDTO) Validate() bool { isOk, _ := regexp.MatchString(rfc2822EmailPattern, trimmedEmail) // TODO: check password for certain special characters - if strings.TrimSpace((*dto).Name) == "" || trimmedEmail == "" || !isOk || len(trimmedPassword) < 8 || len(trimmedPassword) > 16 { + if strings.TrimSpace((*dto).Name) == "" || !isOk || len(trimmedPassword) < 8 || len(trimmedPassword) > 16 { return false } diff --git a/application/dtos/user_validator_test.go b/application/dtos/user_validator_test.go index 748b00c..f574109 100644 --- a/application/dtos/user_validator_test.go +++ b/application/dtos/user_validator_test.go @@ -112,7 +112,7 @@ func TestUserValidateShouldReturnFalseIfEmailIsMalformed(t *testing.T) { assert.False(t, result) } -func TestUserValidateShouldReturnFalseIfIsPasswordIsNil(t *testing.T) { +func TestUserValidateShouldReturnFalseIfPasswordIsNil(t *testing.T) { // Arrange userDTO := UserDTO{ Name: "Test name", @@ -126,7 +126,7 @@ func TestUserValidateShouldReturnFalseIfIsPasswordIsNil(t *testing.T) { assert.False(t, result) } -func TestUserValidateShouldReturnFalseIfIsPasswordIsEmpty(t *testing.T) { +func TestUserValidateShouldReturnFalseIfPasswordIsEmpty(t *testing.T) { // Arrange userDTO := UserDTO{ Name: "Test name", @@ -141,7 +141,7 @@ func TestUserValidateShouldReturnFalseIfIsPasswordIsEmpty(t *testing.T) { assert.False(t, result) } -func TestUserValidateShouldReturnFalseIfIsPasswordIsShorterThanEightCharacters(t *testing.T) { +func TestUserValidateShouldReturnFalseIfPasswordIsShorterThanEightCharacters(t *testing.T) { // Arrange userDTO := UserDTO{ Name: "Test name", @@ -156,7 +156,7 @@ func TestUserValidateShouldReturnFalseIfIsPasswordIsShorterThanEightCharacters(t assert.False(t, result) } -func TestUserValidateShouldReturnFalseIfIsPasswordIsLongerThan16Characters(t *testing.T) { +func TestUserValidateShouldReturnFalseIfPasswordIsLongerThanSixteenCharacters(t *testing.T) { // Arrange userDTO := UserDTO{ Name: "Test name", diff --git a/application/errors/errors.go b/application/errors/errors.go index 830d2c8..48b28a2 100644 --- a/application/errors/errors.go +++ b/application/errors/errors.go @@ -8,6 +8,8 @@ import ( type Errors struct{} -func (_ Errors) FiberValidationError(itemName string) error { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid %s.", itemName)) -} +func (_ Errors) CannotReadFileError(fileName string) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Could not read %s file.", fileName)) } +func (_ Errors) FiberValidationError(itemName string) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid %s.", itemName)) } +func (_ Errors) IncorrectPasswordError() error { return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintln("Incorrect password.")) } +func (_ Errors) ItemNotFoundError(itemName string) error { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("%s not found.", itemName)) } +func (_ Errors) ItemNotParsedError(itemName string) error { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("%s is not in PKCS8 RSA format.", itemName)) } diff --git a/application/services/login_service_impl.go b/application/services/login_service_impl.go new file mode 100644 index 0000000..7109dd8 --- /dev/null +++ b/application/services/login_service_impl.go @@ -0,0 +1,64 @@ +package services + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" + "todoapp.com/application/dtos" + "todoapp.com/application/errors" + "todoapp.com/domain/interfaces" + "todoapp.com/domain/models" +) + +type LoginServiceImpl struct { + userRepository interfaces.UsersRepositoryEmail + config *models.Config +} + +func NewLoginService(userRepository interfaces.UsersRepositoryEmail, config *models.Config) *LoginServiceImpl { + return &LoginServiceImpl{ + userRepository: userRepository, + config: config, + } +} + +func (ls *LoginServiceImpl) Login(context context.Context, login *dtos.LoginDTO) error { + dbUser := ls.userRepository.GetByEmail(context, &login.Email) + + if dbUser.Email == "" { + return errors.Errors{}.ItemNotFoundError("User") + } + if dbUser.Password != (*login).Password { + return errors.Errors{}.IncorrectPasswordError() + } + + return ls.GenerateToken(login) +} + +func (ls *LoginServiceImpl) GenerateToken(login *dtos.LoginDTO) error { + fmt.Println(ls.config.PrivateKeyPath) + fmt.Println("Llego aquĆ­") + encodedKey, error := os.ReadFile(ls.config.PrivateKeyPath) + if error != nil { + return errors.Errors{}.CannotReadFileError("OPENSSH private key") + } + + privateKey, error := jwt.ParseRSAPrivateKeyFromPEM(encodedKey) + if error != nil { + return errors.Errors{}.ItemNotParsedError("OPENSSH private key") + } + + claims := jwt.MapClaims{ + "email": login.Email, + "expiration": time.Now().UTC().Add(time.Hour * 24).Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + + signedToken, error := token.SignedString(privateKey) + login.Token = &signedToken + + return error +} diff --git a/application/services/login_service_impl_test.go b/application/services/login_service_impl_test.go new file mode 100644 index 0000000..70c7e24 --- /dev/null +++ b/application/services/login_service_impl_test.go @@ -0,0 +1,119 @@ +package services + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "todoapp.com/application/dtos" + "todoapp.com/domain/models" +) + +func TestLoginGenerateTokenShouldReturnTokenAndNoError(t *testing.T) { + // Arrange + testLoginDTO := &dtos.LoginDTO{ + Email: "a@a.com", + Password: "aaaaaaaa", + } + testConfig := &models.Config{ + PrivateKeyPath: "../../testing/id_rsa", + PublicKeyPath: "../../testing/id_rsa.pub", + } + MockedUsersRepository := new(MockedUsersRepository) + + // Act + testLoginService := NewLoginService(MockedUsersRepository, testConfig) + error := testLoginService.GenerateToken(testLoginDTO) + + // Assert + assert.Nil(t, error) + assert.NotNil(t, testLoginDTO.Token) + assert.NotEmpty(t, testLoginDTO.Token) +} + +func TestLoginLoginShouldReturnErrorIfGivenPasswordDoesNotMatch(t *testing.T) { + // Arrange + id1 := uint(1) + testUser := models.User{ + ID: &id1, + Name: "test 1", + Email: "a@a.com", + Password: "aaaaaaaa", + } + testLoginDTO := &dtos.LoginDTO{ + Email: "a@a.com", + Password: "bbbbb", + } + testContext := context.Background() + MockedUserRepository := new(MockedUsersRepository) + MockedUserRepository.mock.On("GetByEmail", testContext, &testLoginDTO.Email).Return(testUser) + testConfig := &models.Config{ + PrivateKeyPath: "../../testing/id_rsa", + PublicKeyPath: "../../testing/id_rsa.pub", + } + + // Act + testLoginService := NewLoginService(MockedUserRepository, testConfig) + error := testLoginService.Login(testContext, testLoginDTO) + + // Assert + assert.NotNil(t, error) +} + +func TestLoginLoginShouldReturnErrorIfUserIsNotFound(t *testing.T) { + // Arrange + id1 := uint(1) + testUser := models.User{ + ID: &id1, + Name: "test 1", + Email: "a@a.com", + Password: "aaaaaaaa", + } + testLoginDTO := &dtos.LoginDTO{ + Email: "b@b.com", + Password: "aaaaaaaa", + } + testContext := context.Background() + MockedUserRepository := new(MockedUsersRepository) + MockedUserRepository.mock.On("GetByEmail", testContext, &testLoginDTO.Email).Return(testUser) + testConfig := &models.Config{ + PrivateKeyPath: "../../testing/id_rsa", + PublicKeyPath: "../../testing/id_rsa.pub", + } + + // Act + testLoginService := NewLoginService(MockedUserRepository, testConfig) + error := testLoginService.Login(testContext, testLoginDTO) + + // Assert + assert.NotNil(t, error) +} + +func TestLoginLoginShouldNotReturnAnyErrorsIfEverythingIsOk(t *testing.T) { + // Arrange + id1 := uint(1) + testUser := models.User{ + ID: &id1, + Name: "test 1", + Email: "a@a.com", + Password: "aaaaaaaa", + } + testLoginDTO := &dtos.LoginDTO{ + Email: "a@a.com", + Password: "aaaaaaaa", + } + testContext := context.Background() + MockedUserRepository := new(MockedUsersRepository) + MockedUserRepository.mock.On("GetByEmail", testContext, &testLoginDTO.Email).Return(testUser) + testConfig := &models.Config{ + PrivateKeyPath: "../../testing/id_rsa", + PublicKeyPath: "../../testing/id_rsa.pub", + } + + // Act + testLoginService := NewLoginService(MockedUserRepository, testConfig) + error := testLoginService.Login(testContext, testLoginDTO) + + // Assert + assert.Nil(t, error) +} diff --git a/application/services/todos_service_impl.go b/application/services/todos_service_impl.go index 83e7c9e..a5d723d 100644 --- a/application/services/todos_service_impl.go +++ b/application/services/todos_service_impl.go @@ -10,10 +10,10 @@ import ( ) type TodosServiceImpl struct { - todosRepository interfaces.TodosRepository + todosRepository interfaces.BaseTodosRepository } -func NewTodosService(todosRepository interfaces.TodosRepository) interfaces.TodosService { +func NewTodosService(todosRepository interfaces.BaseTodosRepository) *TodosServiceImpl { return &TodosServiceImpl{todosRepository: todosRepository} } diff --git a/application/services/todos_service_impl_test.go b/application/services/todos_service_impl_test.go index 891ee70..fa5a28d 100644 --- a/application/services/todos_service_impl_test.go +++ b/application/services/todos_service_impl_test.go @@ -40,11 +40,6 @@ func (m *MockedTodosRepository) Delete(context context.Context, model *models.To return args.Error(0) } -// Just to implement TodosRepository interface -func (m *MockedTodosRepository) CleanUp(context context.Context) int64 { - return 0 -} - func TestTodosGetAllShouldReturnTestTodoDTOs(t *testing.T) { // Arrange id1, id2 := uint(1), uint(2) @@ -139,7 +134,7 @@ func TestTodosUpdateShouldReturnNoErrorOnUpdate(t *testing.T) { error := testTodosService.Update(testContext, testTodoDTO) // Assert - assert.Equal(t, error, nil) + assert.Nil(t, error) } func TestTodosDeleteShouldReturnNoErrorOnDelete(t *testing.T) { diff --git a/application/services/users_service_impl.go b/application/services/users_service_impl.go index e923096..e78292d 100644 --- a/application/services/users_service_impl.go +++ b/application/services/users_service_impl.go @@ -10,10 +10,10 @@ import ( ) type UsersServiceImpl struct { - usersRepository interfaces.UsersRepository + usersRepository interfaces.BaseUsersRepositoryWithEmail } -func NewUsersService(usersRepository interfaces.UsersRepository) interfaces.UsersService { +func NewUsersService(usersRepository interfaces.BaseUsersRepositoryWithEmail) *UsersServiceImpl { return &UsersServiceImpl{usersRepository: usersRepository} } @@ -30,9 +30,18 @@ func (us *UsersServiceImpl) GetAll(context context.Context) []dtos.UserDTO { return dtosSlice } -func (us *UsersServiceImpl) Get(context context.Context, id *uint) dtos.UserDTO { +func (us *UsersServiceImpl) GetById(context context.Context, id *uint) dtos.UserDTO { dto := &dtos.UserDTO{} - entity := us.usersRepository.Get(context, id) + entity := us.usersRepository.GetById(context, id) + + dto.From(&entity) + + return (*dto) +} + +func (us *UsersServiceImpl) GetByEmail(context context.Context, email *string) dtos.UserDTO { + dto := &dtos.UserDTO{} + entity := us.usersRepository.GetByEmail(context, email) dto.From(&entity) diff --git a/application/services/users_service_impl_test.go b/application/services/users_service_impl_test.go index 950a1d5..8f27a98 100644 --- a/application/services/users_service_impl_test.go +++ b/application/services/users_service_impl_test.go @@ -20,9 +20,23 @@ func (m *MockedUsersRepository) GetAll(context context.Context) []models.User { return args.Get(0).([]models.User) } -func (m *MockedUsersRepository) Get(context context.Context, id *uint) models.User { +func (m *MockedUsersRepository) GetById(context context.Context, id *uint) models.User { args := m.mock.Called(context, id) + if *id != uint(1) { + return models.User{} + } + + return args.Get(0).(models.User) +} + +func (m *MockedUsersRepository) GetByEmail(context context.Context, email *string) models.User { + args := m.mock.Called(context, email) + + if *email != "a@a.com" { + return models.User{} + } + return args.Get(0).(models.User) } @@ -46,11 +60,6 @@ func (m *MockedUsersRepository) Delete(context context.Context, model *models.Us return args.Error(0) } -// Just to implement UsersRepository interface -func (m *MockedUsersRepository) CleanUp(context context.Context) int64 { - return 0 -} - func TestUsersGetAllShouldReturnTestUserDTOs(t *testing.T) { // Arrange id1, id2 := uint(1), uint(2) @@ -94,9 +103,37 @@ func TestUsersGetAllShouldReturnTestUserDTOs(t *testing.T) { assert.Equal(t, testUserDTOs, result) } -func TestUsersGetShouldReturnTestUserDTO(t *testing.T) { +func TestUsersGetByIdShouldReturnTestUserDTO(t *testing.T) { + // Arrange + id1 := uint(1) + testUser := models.User{ + ID: &id1, + Name: "test name 1", + Email: "a@a.com", + Password: "aaaaaaaa", + } + testUserDTO := dtos.UserDTO{ + ID: &id1, + Name: "test name 1", + Email: "a@a.com", + Password: "", + } + testContext := context.Background() + mockedUsersRepository := new(MockedUsersRepository) + mockedUsersRepository.mock.On("GetById", testContext, &id1).Return(testUser) + + // Act + testUsersService := NewUsersService(mockedUsersRepository) + result := testUsersService.GetById(testContext, &id1) + + // Assert + assert.Equal(t, testUserDTO, result) +} + +func TestUsersGetByEmailShouldReturnTestUserDTO(t *testing.T) { // Arrange id1 := uint(1) + testEmail := "a@a.com" testUser := models.User{ ID: &id1, Name: "test name 1", @@ -111,11 +148,11 @@ func TestUsersGetShouldReturnTestUserDTO(t *testing.T) { } testContext := context.Background() mockedUsersRepository := new(MockedUsersRepository) - mockedUsersRepository.mock.On("Get", testContext, &id1).Return(testUser) + mockedUsersRepository.mock.On("GetByEmail", testContext, &testEmail).Return(testUser) // Act testUsersService := NewUsersService(mockedUsersRepository) - result := testUsersService.Get(testContext, &id1) + result := testUsersService.GetByEmail(testContext, &testEmail) // Assert assert.Equal(t, testUserDTO, result) @@ -172,7 +209,7 @@ func TestUsersUpdateShouldReturnNoErrorOnUpdate(t *testing.T) { error := testUsersService.Update(testContext, testUserDTO) // Assert - assert.Equal(t, error, nil) + assert.Nil(t, error) } func TestUsersDeleteShouldReturnNoErrorOnDelete(t *testing.T) { diff --git a/config.json b/config.json new file mode 100644 index 0000000..dd4ee95 --- /dev/null +++ b/config.json @@ -0,0 +1,4 @@ +{ + "rsa_private": "C:/Users/alexc/.ssh/id_rsa", + "rsa_public": "C:/Users/alexc/.ssh/id_rsa.pub" +} diff --git a/configuration/config.go b/configuration/config.go new file mode 100644 index 0000000..f8e84d0 --- /dev/null +++ b/configuration/config.go @@ -0,0 +1,23 @@ +package configuration + +import ( + "encoding/json" + "fmt" + "log" + "os" + + "todoapp.com/domain/models" +) + +func LoadCfg(config *models.Config) error { + configFile, error := os.ReadFile("config.json") + if error != nil { + log.Fatal("Configuration could not be loaded.") + } + + error = json.Unmarshal(configFile, config) + fmt.Println("Estoy en la config") + fmt.Println(config) + + return error +} diff --git a/domain/interfaces/login_service.go b/domain/interfaces/login_service.go new file mode 100644 index 0000000..eb36874 --- /dev/null +++ b/domain/interfaces/login_service.go @@ -0,0 +1,12 @@ +package interfaces + +import ( + "context" + + "todoapp.com/application/dtos" +) + +type LoginService interface { + Login(context context.Context, login *dtos.LoginDTO) error + GenerateToken(login *dtos.LoginDTO) error +} diff --git a/domain/interfaces/todos_repository.go b/domain/interfaces/todos_repository.go index 573a465..206f707 100644 --- a/domain/interfaces/todos_repository.go +++ b/domain/interfaces/todos_repository.go @@ -6,10 +6,20 @@ import ( "todoapp.com/domain/models" ) -type TodosRepository interface { - GetAll(context context.Context) []models.Todo - Create(context context.Context, model *models.Todo) error - Update(context context.Context, model *models.Todo) error - Delete(context context.Context, model *models.Todo) error - CleanUp(context context.Context) int64 -} +type ( + TodosRepository interface { + BaseTodosRepository + TodosRepositoryCleanUp + } + + BaseTodosRepository interface { + GetAll(context context.Context) []models.Todo + Create(context context.Context, model *models.Todo) error + Update(context context.Context, model *models.Todo) error + Delete(context context.Context, model *models.Todo) error + } + + TodosRepositoryCleanUp interface { + CleanUp(context context.Context) int64 + } +) diff --git a/domain/interfaces/users_repository.go b/domain/interfaces/users_repository.go index 2c2629c..cedbc20 100644 --- a/domain/interfaces/users_repository.go +++ b/domain/interfaces/users_repository.go @@ -6,11 +6,31 @@ import ( "todoapp.com/domain/models" ) -type UsersRepository interface { - GetAll(context context.Context) []models.User - Get(context context.Context, id *uint) models.User - Create(context context.Context, model *models.User) error - Update(context context.Context, model *models.User) error - Delete(context context.Context, model *models.User) error - CleanUp(context context.Context) int64 -} +type ( + UsersRepository interface { + BaseUsersRepository + UsersRepositoryEmail + UsersRepositoryCleanUp + } + + BaseUsersRepositoryWithEmail interface { + BaseUsersRepository + UsersRepositoryEmail + } + + BaseUsersRepository interface { + GetAll(context context.Context) []models.User + GetById(context context.Context, id *uint) models.User + Create(context context.Context, model *models.User) error + Update(context context.Context, model *models.User) error + Delete(context context.Context, model *models.User) error + } + + UsersRepositoryEmail interface { + GetByEmail(context context.Context, email *string) models.User + } + + UsersRepositoryCleanUp interface { + CleanUp(context context.Context) int64 + } +) diff --git a/domain/interfaces/users_service.go b/domain/interfaces/users_service.go index 96bb81a..21dfbd5 100644 --- a/domain/interfaces/users_service.go +++ b/domain/interfaces/users_service.go @@ -6,10 +6,21 @@ import ( "todoapp.com/application/dtos" ) -type UsersService interface { - GetAll(context context.Context) []dtos.UserDTO - Get(context context.Context, id *uint) dtos.UserDTO - Create(context context.Context, dto *dtos.UserDTO) error - Update(context context.Context, dto *dtos.UserDTO) error - Delete(context context.Context, dto *dtos.UserDTO) error -} +type ( + UsersService interface { + BaseUsersService + UsersServiceEmail + } + + BaseUsersService interface { + GetAll(context context.Context) []dtos.UserDTO + GetById(context context.Context, id *uint) dtos.UserDTO + Create(context context.Context, dto *dtos.UserDTO) error + Update(context context.Context, dto *dtos.UserDTO) error + Delete(context context.Context, dto *dtos.UserDTO) error + } + + UsersServiceEmail interface { + GetByEmail(context context.Context, email *string) dtos.UserDTO + } +) diff --git a/domain/models/config.go b/domain/models/config.go new file mode 100644 index 0000000..476369e --- /dev/null +++ b/domain/models/config.go @@ -0,0 +1,6 @@ +package models + +type Config struct { + PrivateKeyPath string `json:"rsa_private"` + PublicKeyPath string `json:"rsa_public"` +} diff --git a/go.mod b/go.mod index 82278ee..cac715e 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,13 @@ go 1.21 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect + github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gofiber/fiber/v2 v2.49.1 // indirect + github.com/gofiber/contrib/jwt v1.0.7 // indirect + github.com/gofiber/fiber/v2 v2.49.2 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/gomodule/redigo v1.8.9 // indirect github.com/google/uuid v1.3.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -20,6 +23,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/stretchr/objx v0.5.0 // indirect @@ -29,7 +33,7 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect github.com/yuin/gopher-lua v1.1.0 // indirect golang.org/x/crypto v0.8.0 // indirect - golang.org/x/sys v0.11.0 // indirect + golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.9.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/postgres v1.5.2 // indirect diff --git a/go.sum b/go.sum index 367e5e7..2071f05 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= +github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= @@ -15,8 +17,14 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofiber/contrib/jwt v1.0.7 h1:LZuCnjEq8AjiDTUjBQSd2zg3H5uDWjHxSXjo7nj9iAc= +github.com/gofiber/contrib/jwt v1.0.7/go.mod h1:fA1apg9zQlUhax+Foc0BHATCDzBsemga1Yr9X0KSvrQ= github.com/gofiber/fiber/v2 v2.49.1 h1:0W2DRWevSirc8pJl4o8r8QejDR8TV6ZUCawHxwbIdOk= github.com/gofiber/fiber/v2 v2.49.1/go.mod h1:nPUeEBUeeYGgwbDm59Gp7vS8MDyScL6ezr/Np9A13WU= +github.com/gofiber/fiber/v2 v2.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs= +github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= @@ -42,6 +50,8 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= @@ -71,6 +81,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/infrastructure/repositories/todos_repository_impl.go b/infrastructure/repositories/todos_repository_impl.go index 187c238..1d6df7b 100644 --- a/infrastructure/repositories/todos_repository_impl.go +++ b/infrastructure/repositories/todos_repository_impl.go @@ -4,7 +4,6 @@ import ( "context" "gorm.io/gorm" - "todoapp.com/domain/interfaces" "todoapp.com/domain/models" ) @@ -12,7 +11,7 @@ type todosRepositoryImpl struct { db *gorm.DB } -func NewTodosRepository(db *gorm.DB) interfaces.TodosRepository { +func NewTodosRepository(db *gorm.DB) *todosRepositoryImpl { return &todosRepositoryImpl{db: db} } diff --git a/infrastructure/repositories/users_repository_impl.go b/infrastructure/repositories/users_repository_impl.go index 8a81c7d..dd3e198 100644 --- a/infrastructure/repositories/users_repository_impl.go +++ b/infrastructure/repositories/users_repository_impl.go @@ -4,7 +4,6 @@ import ( "context" "gorm.io/gorm" - "todoapp.com/domain/interfaces" "todoapp.com/domain/models" ) @@ -12,7 +11,7 @@ type usersRepositoryImpl struct { db *gorm.DB } -func NewUsersRepository(db *gorm.DB) interfaces.UsersRepository { +func NewUsersRepository(db *gorm.DB) *usersRepositoryImpl { return &usersRepositoryImpl{db: db} } @@ -24,7 +23,7 @@ func (ur *usersRepositoryImpl) GetAll(context context.Context) []models.User { return *users } -func (ur *usersRepositoryImpl) Get(context context.Context, id *uint) models.User { +func (ur *usersRepositoryImpl) GetById(context context.Context, id *uint) models.User { user := &models.User{} ur.db.WithContext(context).Where("id = ? and deleted_at is null", (*id)).Find(&user) @@ -32,6 +31,14 @@ func (ur *usersRepositoryImpl) Get(context context.Context, id *uint) models.Use return *user } +func (ur *usersRepositoryImpl) GetByEmail(context context.Context, email *string) models.User { + user := &models.User{} + + ur.db.WithContext(context).Where("email = ? and deleted_at is null", email).Find(&user) + + return *user +} + func (ur *usersRepositoryImpl) Create(context context.Context, user *models.User) error { return ur.db.WithContext(context).Create(user).Error } diff --git a/main.go b/main.go index e744531..03934e3 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,8 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/logger" "todoapp.com/application/services" + "todoapp.com/configuration" + "todoapp.com/domain/models" "todoapp.com/infrastructure/connectors" "todoapp.com/infrastructure/environments" "todoapp.com/infrastructure/repositories" @@ -19,6 +21,12 @@ func main() { log.Fatal("Error loading .env file.") } + var config models.Config + error = configuration.LoadCfg(&config) + if error != nil { + log.Fatal("Error decoding loaded configuration file.") + } + db := connectors.Postgre{}.Connect() app := fiber.New(fiber.Config{JSONEncoder: json.Marshal, JSONDecoder: json.Unmarshal}) @@ -31,14 +39,17 @@ func main() { // services todosService := services.NewTodosService(todosRepository) usersService := services.NewUsersService(usersRepository) + loginService := services.NewLoginService(usersRepository, &config) // controllers todosController := controllers.NewTodosController(todosService) usersController := controllers.NewUsersController(usersService) + loginController := controllers.NewLoginController(loginService) // routes todosController.Route(api) usersController.Route(api) + loginController.Route(api) app.Listen(":3000") } diff --git a/presentation/controllers/login_controller.go b/presentation/controllers/login_controller.go new file mode 100644 index 0000000..23de09c --- /dev/null +++ b/presentation/controllers/login_controller.go @@ -0,0 +1,45 @@ +package controllers + +import ( + "github.com/gofiber/fiber/v2" + "todoapp.com/application/dtos" + "todoapp.com/domain/interfaces" + customErrors "todoapp.com/presentation/errors" + "todoapp.com/presentation/messages" +) + +type LoginController struct { + loginService interfaces.LoginService +} + +func NewLoginController(loginService interfaces.LoginService) *LoginController { + return &LoginController{loginService: loginService} +} + +func (lc *LoginController) Route(router fiber.Router) { + usersRouter := router.Group("/login") + + usersRouter.Post("/", lc.Login) +} + +func (lc *LoginController) Login(context *fiber.Ctx) error { + newCredentials := &dtos.LoginDTO{} + error := context.BodyParser(newCredentials) + if error != nil { + return context.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "message": messages.Messages{}.ParsingErrorMessage("Credentials"), + "status": messages.Status{}.Error(), + }) + } + + error = lc.loginService.Login(context.Context(), newCredentials) + if error != nil { + return customErrors.Errors{}.HandleFiberError(newCredentials, context, error) + } + + return context.Status(fiber.StatusOK).JSON(fiber.Map{ + "data": newCredentials.Token, + "message": messages.Messages{}.LoggedInSuccessfullyMessage(), + "status": messages.Status{}.Success(), + }) +} diff --git a/presentation/controllers/todos_controller.go b/presentation/controllers/todos_controller.go index ef2a6ec..a20abea 100644 --- a/presentation/controllers/todos_controller.go +++ b/presentation/controllers/todos_controller.go @@ -40,7 +40,7 @@ func (tc *TodosController) GetAll(context *fiber.Ctx) error { return context.Status(fiber.StatusOK).JSON(fiber.Map{ "data": todoDTOs, - "message": messages.Messages{}.ReturningItemsMessage(len(todoDTOs), "todo"), + "message": messages.Messages{}.ReturningItemsSuccessfullyMessage(len(todoDTOs), "todo"), "status": messages.Status{}.Success(), }) } @@ -62,7 +62,7 @@ func (tc *TodosController) Create(context *fiber.Ctx) error { return context.Status(fiber.StatusCreated).JSON(fiber.Map{ "data": newTodo, - "message": messages.Messages{}.ItemCreatedMessage(newTodo), + "message": messages.Messages{}.ItemCreatedSuccessfullyMessage(newTodo), "status": messages.Status{}.Success(), }) @@ -91,7 +91,7 @@ func (tc *TodosController) Update(context *fiber.Ctx) error { return context.Status(fiber.StatusOK).JSON(fiber.Map{ "data": todoToUpdate, - "message": messages.Messages{}.ItemCreatedMessage(todoToUpdate), + "message": messages.Messages{}.ItemCreatedSuccessfullyMessage(todoToUpdate), "status": messages.Status{}.Success(), }) diff --git a/presentation/controllers/users_controller.go b/presentation/controllers/users_controller.go index de17d60..d72ca7d 100644 --- a/presentation/controllers/users_controller.go +++ b/presentation/controllers/users_controller.go @@ -41,7 +41,7 @@ func (uc *UsersController) GetAll(context *fiber.Ctx) error { return context.Status(fiber.StatusOK).JSON(fiber.Map{ "data": userDTOs, - "message": messages.Messages{}.ReturningItemsMessage(len(userDTOs), "user"), + "message": messages.Messages{}.ReturningItemsSuccessfullyMessage(len(userDTOs), "user"), "status": messages.Status{}.Success(), }) } @@ -53,11 +53,11 @@ func (uc *UsersController) Get(context *fiber.Ctx) error { } id := uint(i) - userDTO := uc.usersService.Get(context.Context(), &id) + userDTO := uc.usersService.GetById(context.Context(), &id) return context.Status(fiber.StatusOK).JSON(fiber.Map{ "data": userDTO, - "message": messages.Messages{}.ReturningItemsMessage(1, "user"), + "message": messages.Messages{}.ReturningItemsSuccessfullyMessage(1, "user"), "status": messages.Status{}.Success(), }) } @@ -79,7 +79,7 @@ func (uc *UsersController) Create(context *fiber.Ctx) error { return context.Status(fiber.StatusCreated).JSON(fiber.Map{ "data": newUser, - "message": messages.Messages{}.ItemCreatedMessage(newUser), + "message": messages.Messages{}.ItemCreatedSuccessfullyMessage(newUser), "status": messages.Status{}.Success(), }) @@ -108,7 +108,7 @@ func (uc *UsersController) Update(context *fiber.Ctx) error { return context.Status(fiber.StatusOK).JSON(fiber.Map{ "data": userToUpdate, - "message": messages.Messages{}.ItemCreatedMessage(userToUpdate), + "message": messages.Messages{}.ItemCreatedSuccessfullyMessage(userToUpdate), "status": messages.Status{}.Success(), }) diff --git a/presentation/errors/errors.go b/presentation/errors/errors.go index 74fc315..6e76ef3 100644 --- a/presentation/errors/errors.go +++ b/presentation/errors/errors.go @@ -21,7 +21,7 @@ func (_ Errors) HandleFiberError(item interface{}, context *fiber.Ctx, error err } func (_ Errors) IdConflictError(context *fiber.Ctx, itemName string) error { - return context.Status(fiber.StatusBadRequest).JSON(fiber.Map{"message": messages.Messages{}.IdConflictMessage(itemName), "status": messages.Status{}.Error()}) + return context.Status(fiber.StatusBadRequest).JSON(fiber.Map{"message": messages.Messages{}.IdConflictErrorMessage(itemName), "status": messages.Status{}.Error()}) } func (_ Errors) ParsingError(context *fiber.Ctx, itemName string) error { diff --git a/presentation/messages/messages.go b/presentation/messages/messages.go index 25bb293..8b48e12 100644 --- a/presentation/messages/messages.go +++ b/presentation/messages/messages.go @@ -7,13 +7,14 @@ type Status struct{} // Messages func (_ Messages) CollectionEmptyMessage(itemName string) string { return fmt.Sprintf("The %s collection is empty.", itemName) } -func (_ Messages) ItemCreatedMessage(item interface{}) string { return fmt.Sprintf("%v was created successfully.", item) } +func (_ Messages) IdConflictErrorMessage(itemName string) string { return fmt.Sprintf("The route id and the %s's id are not equal.", itemName) } +func (_ Messages) ItemCreatedSuccessfullyMessage(item interface{}) string { return fmt.Sprintf("%v was created successfully.", item) } +func (_ Messages) ItemDeletedSuccessfullyMessage(itemName string, id uint) string { return fmt.Sprintf("%s with id %d was deleted successfully.", itemName , id) } +func (_ Messages) LoggedInSuccessfullyMessage() string { return fmt.Sprintln("Logged in successfully.") } func (_ Messages) ParsingErrorMessage(itemName string) string { return fmt.Sprintf("The provided %s could not be parsed.", itemName) } -func (_ Messages) ReturningItemsMessage(length int, itemName string) string { return fmt.Sprintf("Returning %d %s(s).", length, itemName) } +func (_ Messages) ReturningItemsSuccessfullyMessage(length int, itemName string) string { return fmt.Sprintf("Returning %d %s(s).", length, itemName) } func (_ Messages) RouteFormatErrorMessage(parameter string) string { return fmt.Sprintf("Incorrect format in route's '%s' parameter.", parameter) } -func (_ Messages) IdConflictMessage(itemName string) string { return fmt.Sprintf("The route id and the %s's id are not equal.", itemName) } func (_ Messages) ValuesUpdatedSuccessfullyMessage(itemName string) string { return fmt.Sprintf("%s values updated successfully.", itemName) } -func (_ Messages) ItemDeletedSuccessfullyMessage(itemName string, id uint) string { return fmt.Sprintf("%s with id %d was deleted successfully.", itemName , id) } // Status func (_ Status) Error() string { return "error" } diff --git a/testing/id_rsa b/testing/id_rsa new file mode 100644 index 0000000..e48620a --- /dev/null +++ b/testing/id_rsa @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDcisPHkeIvch5G +1hkWUbuzFxOnDG5N7Hm2vdE78tblQiORrGMZ9eUm46vV+GsxpWVYilXiT3sAAPQZ +moIIKlHhedGW0AvLv6qMplJtkFdY26dUsa+lz4F0+/icqXU2p7r4TFgkyRZ9a8On +ooVhBZy3IlUA+/11gY9Hc+Lb5XvRSPV5J62pbaA3Qd7IG3uwBD2A6yISrfAfXZLg +BSfuI/nk388bDAI/XqZNkvp1V6NMx8BS8Kf3bWgt3nksFBZKAbdyzF5X6KZqeLXl +NRcXwBhh5HH7XzbWv/cIqi8wOAzfXCxEVFpzeN/uMmRca7zr0WSWCV6JWmEkawfJ +bphknMfBAgMBAAECggEBAJndakfi/LXzXTI4jtomfditofVq084jzKr09XRosQrT +wPMSHTXwe0ZNkwY+QbMqXHGQWekBYKQZBjSSumOt+uBfWB1q2r/04a0tINb13CrP +XIfXS99XzU7ruIC8LV3LWcN/qANKFBm+xItA9ONCwhp2pm2AYU3I7UiBjmWyTlcs +zxTq8FuJepQ75iOo1BndUFbaWbhyTXbAQ5xoiGTEt6njeckD+npIOfjD4UXZFT+t +0JCYv+Q3unWle/SklCVAGvhrTt62fMxRjxKSBvQmmm+e2qzwuu2wxqOE389SIdaD +zIdu99CdBQR0hs7ln1ShMfboiYlXTvQKUr7dcjFrp0ECgYEA8aZemnD5bMHdpZZI +362bVEvdwGB7g2HHcvgNkqo5wdcOhIqOOX/OaK7zYkV6EL6vrzHjShyfqV1hjvOJ +lcDfZ4BTU4RRWxuF7ndhaWeLspbg+DzWmFES+YJfJeE3v73Lz/giXvzW436iCeVl +mSi1irIQwbOWIot2bTOifx0qt9kCgYEA6aOCCMaeCRwek4IEMeUB6t6C5PhEeeog +oQZipDC7e3TFGq36BY/8QP7udfkyD1FBcn9yzitiuqtJ+PNxDO97KobMVgO0ysZX +9fMnWJKEGcq3F+BGsriEBWaPo2Pf05u2Vn2AlCYNyM4tXN6/VZXsJoczGimHCQwe +Tk9RG06ARikCgYA1pT55QL1OlJc1DHDvHyZNh24aKBEjcJCLiF/TAHFEBA8YA35h +a2sSOEyVs2DO2NY9qXCQ4lvbiHyA9LXFhgTSgF1/O++nryuDbgM1GCSeJ/qXgUIO +nGj+9R9UVHRA38ygRbCzr6Ow60rjsYZlgvESckdgCRM6ZgWLZpwbZgk2uQKBgAQA +5EKIPL8FN1Tpvm6ocO74Xx/TTUEVjPeVZ21O1HeaGaKKZqVfwT/P3oAxA/WVO9zd +aDc2MRvnwX29litVkzO4WZoDuD5dRbaWMw9me1MB0T9cfXmkhcad6kovdO9oVKiI +wAJJ4KSXO9nCi17JEeHbIToKiiDSj1ZiL46bOsF5AoGBAKMroFjCNJu2bzMT7hxK +GDhPHqMu/fLEmvLDZTftDRLWxsN9M3Ib3ry86JJpBzxw876qG+UvRsp0Vo0dnz4j +D9VZsFLWXQC60uf9MbPoX/RTmXSZxzlhv6ATBSnXNOXn9P8HG+WVBw1+lSp4amku +EecSL8jvOFfDjksbhAkC0sYV +-----END PRIVATE KEY----- diff --git a/testing/id_rsa.pub b/testing/id_rsa.pub new file mode 100644 index 0000000..698cc8a --- /dev/null +++ b/testing/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDcisPHkeIvch5G1hkWUbuzFxOnDG5N7Hm2vdE78tblQiORrGMZ9eUm46vV+GsxpWVYilXiT3sAAPQZmoIIKlHhedGW0AvLv6qMplJtkFdY26dUsa+lz4F0+/icqXU2p7r4TFgkyRZ9a8OnooVhBZy3IlUA+/11gY9Hc+Lb5XvRSPV5J62pbaA3Qd7IG3uwBD2A6yISrfAfXZLgBSfuI/nk388bDAI/XqZNkvp1V6NMx8BS8Kf3bWgt3nksFBZKAbdyzF5X6KZqeLXlNRcXwBhh5HH7XzbWv/cIqi8wOAzfXCxEVFpzeN/uMmRca7zr0WSWCV6JWmEkawfJbphknMfB alexc@Mzdlx