v2 #43
|
@ -0,0 +1,76 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/config"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthorService struct {
|
||||||
|
repo repository.AuthorRepository
|
||||||
|
config config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthorService(repo repository.AuthorRepository, config config.Config) *AuthorService {
|
||||||
|
return &AuthorService{repo: repo, config: config}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) Create(name string, password string) (*model.Author, error) {
|
||||||
|
hash, err := hashPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.repo.Create(name, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) FindByName(name string) (*model.Author, error) {
|
||||||
|
return s.repo.FindByName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) Authenticate(name string, password string) bool {
|
||||||
|
author, err := s.repo.FindByName(name)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(author.PasswordHash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) getSecretKey() string {
|
||||||
|
return s.config.SECRET_KEY()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) CreateToken(name string) (string, error) {
|
||||||
|
hash := sha256.New()
|
||||||
|
_, err := hash.Write([]byte(name + s.getSecretKey()))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s.%x", name, hash.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorService) ValidateToken(token string) bool {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
witness := parts[len(parts)-1]
|
||||||
|
name := strings.Join(parts[:len(parts)-1], ".")
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
_, err := hash.Write([]byte(name + s.getSecretKey()))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", hash.Sum(nil)) == witness
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package app_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testConfig struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testConfig) SECRET_KEY() string {
|
||||||
|
return "test"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAutherService() *app.AuthorService {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
authorRepo := infra.NewDefaultAuthorRepo(db)
|
||||||
|
authorService := app.NewAuthorService(authorRepo, &testConfig{})
|
||||||
|
return authorService
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorCreate(t *testing.T) {
|
||||||
|
authorService := getAutherService()
|
||||||
|
author, err := authorService.Create("test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test", author.Name)
|
||||||
|
require.NotEmpty(t, author.PasswordHash)
|
||||||
|
require.NotEqual(t, "test", author.PasswordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorFindByName(t *testing.T) {
|
||||||
|
authorService := getAutherService()
|
||||||
|
_, err := authorService.Create("test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
author, err := authorService.FindByName("test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test", author.Name)
|
||||||
|
require.NotEmpty(t, author.PasswordHash)
|
||||||
|
require.NotEqual(t, "test", author.PasswordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorAuthenticate(t *testing.T) {
|
||||||
|
authorService := getAutherService()
|
||||||
|
_, err := authorService.Create("test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, authorService.Authenticate("test", "test"))
|
||||||
|
require.False(t, authorService.Authenticate("test", "test1"))
|
||||||
|
require.False(t, authorService.Authenticate("test1", "test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorCreateToken(t *testing.T) {
|
||||||
|
authorService := getAutherService()
|
||||||
|
_, err := authorService.Create("test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
token, err := authorService.CreateToken("test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, token)
|
||||||
|
require.NotEqual(t, "test", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorValidateToken(t *testing.T) {
|
||||||
|
authorService := getAutherService()
|
||||||
|
_, err := authorService.Create("test", "test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
token, err := authorService.CreateToken("test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.True(t, authorService.ValidateToken(token))
|
||||||
|
require.False(t, authorService.ValidateToken(token[:len(token)-2]))
|
||||||
|
require.False(t, authorService.ValidateToken("test"))
|
||||||
|
require.False(t, authorService.ValidateToken("test.test"))
|
||||||
|
require.False(t, authorService.ValidateToken(strings.Replace(token, "test", "test1", 1)))
|
||||||
|
}
|
|
@ -17,3 +17,8 @@ type BinaryRepository interface {
|
||||||
Create(name string, data []byte) (*model.BinaryFile, error)
|
Create(name string, data []byte) (*model.BinaryFile, error)
|
||||||
FindById(id string) (*model.BinaryFile, error)
|
FindById(id string) (*model.BinaryFile, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthorRepository interface {
|
||||||
|
Create(name string, passwordHash string) (*model.Author, error)
|
||||||
|
FindByName(name string) (*model.Author, error)
|
||||||
|
}
|
||||||
|
|
|
@ -4,46 +4,64 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/rand"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"owl-blogs/app"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/infra"
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/test"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testDbName() string {
|
func getUserToken(service *app.AuthorService) string {
|
||||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
_, err := service.Create("test", "test")
|
||||||
rand.Seed(time.Now().UnixNano())
|
if err != nil {
|
||||||
b := make([]rune, 6)
|
panic(err)
|
||||||
for i := range b {
|
|
||||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
|
||||||
}
|
}
|
||||||
return "/tmp/" + string(b) + ".db"
|
token, err := service.CreateToken("test")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEditorFormGet(t *testing.T) {
|
func TestEditorFormGet(t *testing.T) {
|
||||||
db := infra.NewSqliteDB(testDbName())
|
db := test.NewMockDb()
|
||||||
app := App(db).FiberApp
|
owlApp := App(db)
|
||||||
|
app := owlApp.FiberApp
|
||||||
|
token := getUserToken(owlApp.AuthorService)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/editor/ImageEntry", nil)
|
req := httptest.NewRequest("GET", "/editor/ImageEntry", nil)
|
||||||
|
req.AddCookie(&http.Cookie{Name: "token", Value: token})
|
||||||
resp, err := app.Test(req)
|
resp, err := app.Test(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEditorFormPost(t *testing.T) {
|
func TestEditorFormGetNoAuth(t *testing.T) {
|
||||||
dbName := testDbName()
|
db := test.NewMockDb()
|
||||||
db := infra.NewSqliteDB(dbName)
|
|
||||||
owlApp := App(db)
|
owlApp := App(db)
|
||||||
app := owlApp.FiberApp
|
app := owlApp.FiberApp
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/editor/ImageEntry", nil)
|
||||||
|
req.AddCookie(&http.Cookie{Name: "token", Value: "invalid"})
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 302, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditorFormPost(t *testing.T) {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
owlApp := App(db)
|
||||||
|
app := owlApp.FiberApp
|
||||||
|
token := getUserToken(owlApp.AuthorService)
|
||||||
repo := infra.NewEntryRepository(db, owlApp.Registry)
|
repo := infra.NewEntryRepository(db, owlApp.Registry)
|
||||||
binRepo := infra.NewBinaryFileRepo(db)
|
binRepo := infra.NewBinaryFileRepo(db)
|
||||||
|
|
||||||
|
@ -67,6 +85,7 @@ func TestEditorFormPost(t *testing.T) {
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/editor/ImageEntry", body)
|
req := httptest.NewRequest("POST", "/editor/ImageEntry", body)
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
req.AddCookie(&http.Cookie{Name: "token", Value: token})
|
||||||
resp, err := app.Test(req)
|
resp, err := app.Test(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 302, resp.StatusCode)
|
require.Equal(t, 302, resp.StatusCode)
|
||||||
|
@ -84,3 +103,34 @@ func TestEditorFormPost(t *testing.T) {
|
||||||
require.Equal(t, fileBytes, bin.Data)
|
require.Equal(t, fileBytes, bin.Data)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEditorFormPostNoAuth(t *testing.T) {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
owlApp := App(db)
|
||||||
|
app := owlApp.FiberApp
|
||||||
|
|
||||||
|
fileDir, _ := os.Getwd()
|
||||||
|
fileName := "../../test/fixtures/test.png"
|
||||||
|
filePath := path.Join(fileDir, fileName)
|
||||||
|
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
part, _ := writer.CreateFormFile("ImageId", filepath.Base(file.Name()))
|
||||||
|
io.Copy(part, file)
|
||||||
|
part, _ = writer.CreateFormField("Content")
|
||||||
|
io.WriteString(part, "test content")
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/editor/ImageEntry", body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
req.AddCookie(&http.Cookie{Name: "token", Value: "invalid"})
|
||||||
|
resp, err := app.Test(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 302, resp.StatusCode)
|
||||||
|
require.Contains(t, resp.Header.Get("Location"), "/auth/login")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
|
"owl-blogs/config"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/infra"
|
"owl-blogs/infra"
|
||||||
"owl-blogs/web"
|
"owl-blogs/web"
|
||||||
|
@ -10,16 +11,20 @@ import (
|
||||||
const DbPath = "owlblogs.db"
|
const DbPath = "owlblogs.db"
|
||||||
|
|
||||||
func App(db infra.Database) *web.WebApp {
|
func App(db infra.Database) *web.WebApp {
|
||||||
registry := app.NewEntryTypeRegistry()
|
config := config.NewConfig()
|
||||||
|
|
||||||
|
registry := app.NewEntryTypeRegistry()
|
||||||
registry.Register(&model.ImageEntry{})
|
registry.Register(&model.ImageEntry{})
|
||||||
|
|
||||||
entryRepo := infra.NewEntryRepository(db, registry)
|
entryRepo := infra.NewEntryRepository(db, registry)
|
||||||
binRepo := infra.NewBinaryFileRepo(db)
|
binRepo := infra.NewBinaryFileRepo(db)
|
||||||
|
authorRepo := infra.NewDefaultAuthorRepo(db)
|
||||||
|
|
||||||
entryService := app.NewEntryService(entryRepo)
|
entryService := app.NewEntryService(entryRepo)
|
||||||
binaryService := app.NewBinaryFileService(binRepo)
|
binaryService := app.NewBinaryFileService(binRepo)
|
||||||
return web.NewWebApp(entryService, registry, binaryService)
|
authorService := app.NewAuthorService(authorRepo, config)
|
||||||
|
|
||||||
|
return web.NewWebApp(entryService, registry, binaryService, authorService)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
type Config interface {
|
||||||
|
SECRET_KEY() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnvConfig struct {
|
||||||
|
secretKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvOrPanic(key string) string {
|
||||||
|
value, set := os.LookupEnv(key)
|
||||||
|
if !set {
|
||||||
|
panic("Environment variable " + key + " is not set")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig() Config {
|
||||||
|
return &EnvConfig{
|
||||||
|
secretKey: getEnvOrPanic("OWL_SECRET_KEY"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *EnvConfig) SECRET_KEY() string {
|
||||||
|
return c.secretKey
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
Name string
|
||||||
|
PasswordHash string
|
||||||
|
}
|
3
go.mod
3
go.mod
|
@ -24,6 +24,7 @@ require (
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.47.0 // indirect
|
github.com/valyala/fasthttp v1.47.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.9.0 // indirect
|
golang.org/x/crypto v0.11.0 // indirect
|
||||||
|
golang.org/x/sys v0.10.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -58,6 +58,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||||
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
@ -83,6 +85,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||||
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sqlAuthor struct {
|
||||||
|
Name string `db:"name"`
|
||||||
|
PasswordHash string `db:"password_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultAuthorRepo struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultAuthorRepo(db Database) *DefaultAuthorRepo {
|
||||||
|
sqlxdb := db.Get()
|
||||||
|
|
||||||
|
// Create table if not exists
|
||||||
|
sqlxdb.MustExec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS authors (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
password_hash TEXT NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
return &DefaultAuthorRepo{
|
||||||
|
db: sqlxdb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByName implements repository.AuthorRepository.
|
||||||
|
func (r *DefaultAuthorRepo) FindByName(name string) (*model.Author, error) {
|
||||||
|
var author sqlAuthor
|
||||||
|
err := r.db.Get(&author, "SELECT * FROM authors WHERE name = ?", name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.Author{
|
||||||
|
Name: author.Name,
|
||||||
|
PasswordHash: author.PasswordHash,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create implements repository.AuthorRepository.
|
||||||
|
func (r *DefaultAuthorRepo) Create(name string, passwordHash string) (*model.Author, error) {
|
||||||
|
author := sqlAuthor{
|
||||||
|
Name: name,
|
||||||
|
PasswordHash: passwordHash,
|
||||||
|
}
|
||||||
|
_, err := r.db.NamedExec("INSERT INTO authors (name, password_hash) VALUES (:name, :password_hash)", author)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.Author{
|
||||||
|
Name: author.Name,
|
||||||
|
PasswordHash: author.PasswordHash,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package infra_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupAutherRepo() repository.AuthorRepository {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
repo := infra.NewDefaultAuthorRepo(db)
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorRepoCreate(t *testing.T) {
|
||||||
|
repo := setupAutherRepo()
|
||||||
|
|
||||||
|
author, err := repo.Create("name", "password")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
author, err = repo.FindByName(author.Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, author.Name, "name")
|
||||||
|
require.Equal(t, author.PasswordHash, "password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorRepoNoSideEffect(t *testing.T) {
|
||||||
|
repo := setupAutherRepo()
|
||||||
|
|
||||||
|
author, err := repo.Create("name1", "password1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
author2, err := repo.Create("name2", "password2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
author, err = repo.FindByName(author.Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
author2, err = repo.FindByName(author2.Name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, author.Name, "name1")
|
||||||
|
require.Equal(t, author.PasswordHash, "password1")
|
||||||
|
require.Equal(t, author2.Name, "name2")
|
||||||
|
require.Equal(t, author2.PasswordHash, "password2")
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
OWL_SECRET_KEY=test-secret-key \
|
||||||
|
go test -v -coverprofile=coverage.out ./...
|
27
web/app.go
27
web/app.go
|
@ -2,6 +2,7 @@ package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
|
"owl-blogs/web/middleware"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
@ -11,9 +12,15 @@ type WebApp struct {
|
||||||
EntryService *app.EntryService
|
EntryService *app.EntryService
|
||||||
BinaryService *app.BinaryService
|
BinaryService *app.BinaryService
|
||||||
Registry *app.EntryTypeRegistry
|
Registry *app.EntryTypeRegistry
|
||||||
|
AuthorService *app.AuthorService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebApp(entryService *app.EntryService, typeRegistry *app.EntryTypeRegistry, binService *app.BinaryService) *WebApp {
|
func NewWebApp(
|
||||||
|
entryService *app.EntryService,
|
||||||
|
typeRegistry *app.EntryTypeRegistry,
|
||||||
|
binService *app.BinaryService,
|
||||||
|
authorService *app.AuthorService,
|
||||||
|
) *WebApp {
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
|
||||||
indexHandler := NewIndexHandler(entryService)
|
indexHandler := NewIndexHandler(entryService)
|
||||||
|
@ -25,15 +32,20 @@ func NewWebApp(entryService *app.EntryService, typeRegistry *app.EntryTypeRegist
|
||||||
editorListHandler := NewEditorListHandler(typeRegistry)
|
editorListHandler := NewEditorListHandler(typeRegistry)
|
||||||
editorHandler := NewEditorHandler(entryService, typeRegistry, binService)
|
editorHandler := NewEditorHandler(entryService, typeRegistry, binService)
|
||||||
|
|
||||||
|
// Login
|
||||||
|
app.Get("/auth/login", loginHandler.HandleGet)
|
||||||
|
app.Post("/auth/login", loginHandler.HandlePost)
|
||||||
|
|
||||||
|
// Editor
|
||||||
|
editor := app.Group("/editor")
|
||||||
|
editor.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
||||||
|
editor.Get("/", editorListHandler.Handle)
|
||||||
|
editor.Get("/:editor/", editorHandler.HandleGet)
|
||||||
|
editor.Post("/:editor/", editorHandler.HandlePost)
|
||||||
|
|
||||||
// app.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
|
// app.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
|
||||||
app.Get("/", indexHandler.Handle)
|
app.Get("/", indexHandler.Handle)
|
||||||
app.Get("/lists/:list/", listHandler.Handle)
|
app.Get("/lists/:list/", listHandler.Handle)
|
||||||
// Editor
|
|
||||||
app.Get("/editor/auth/", loginHandler.HandleGet)
|
|
||||||
app.Post("/editor/auth/", loginHandler.HandlePost)
|
|
||||||
app.Get("/editor/", editorListHandler.Handle)
|
|
||||||
app.Get("/editor/:editor/", editorHandler.HandleGet)
|
|
||||||
app.Post("/editor/:editor/", editorHandler.HandlePost)
|
|
||||||
// Media
|
// Media
|
||||||
app.Get("/media/*filepath", mediaHandler.Handle)
|
app.Get("/media/*filepath", mediaHandler.Handle)
|
||||||
// RSS
|
// RSS
|
||||||
|
@ -57,6 +69,7 @@ func NewWebApp(entryService *app.EntryService, typeRegistry *app.EntryTypeRegist
|
||||||
EntryService: entryService,
|
EntryService: entryService,
|
||||||
Registry: typeRegistry,
|
Registry: typeRegistry,
|
||||||
BinaryService: binService,
|
BinaryService: binService,
|
||||||
|
AuthorService: authorService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,16 @@ type EditorHandler struct {
|
||||||
registry *app.EntryTypeRegistry
|
registry *app.EntryTypeRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEditorHandler(entryService *app.EntryService, registry *app.EntryTypeRegistry, binService *app.BinaryService) *EditorHandler {
|
func NewEditorHandler(
|
||||||
return &EditorHandler{entrySvc: entryService, registry: registry, binSvc: binService}
|
entryService *app.EntryService,
|
||||||
|
registry *app.EntryTypeRegistry,
|
||||||
|
binService *app.BinaryService,
|
||||||
|
) *EditorHandler {
|
||||||
|
return &EditorHandler{
|
||||||
|
entrySvc: entryService,
|
||||||
|
registry: registry,
|
||||||
|
binSvc: binService,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *EditorHandler) paramToEntry(c *fiber.Ctx) (model.Entry, error) {
|
func (h *EditorHandler) paramToEntry(c *fiber.Ctx) (model.Entry, error) {
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthMiddleware struct {
|
||||||
|
authorService *app.AuthorService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthMiddleware(authorService *app.AuthorService) *AuthMiddleware {
|
||||||
|
return &AuthMiddleware{authorService: authorService}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthMiddleware) Handle(c *fiber.Ctx) error {
|
||||||
|
// get token from cookie
|
||||||
|
token := c.Cookies("token")
|
||||||
|
if token == "" {
|
||||||
|
return c.Redirect("/auth/login")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check token
|
||||||
|
valid := m.authorService.ValidateToken(token)
|
||||||
|
if !valid {
|
||||||
|
return c.Redirect("/auth/login")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
Loading…
Reference in New Issue