Compare commits

...

50 Commits

Author SHA1 Message Date
Lars Hoogestraat 25811479a8 updated deps 2023-12-26 23:59:50 +01:00
Lars Hoogestraat becd502fbc updated deps 2023-09-01 00:51:15 +02:00
Lars Hoogestraat a82a476f67 updated deps 2023-02-25 16:05:00 +01:00
Lars Hoogestraat 0b4f8379b5 optimization for css structure 2022-08-02 23:48:27 +02:00
Lars Hoogestraat db4f41f1ac updated tests and deps 2022-05-04 18:56:19 +02:00
Lars Hoogestraat 8452e8bd52 Merge branch 'develop' 2022-01-19 23:47:33 +01:00
Lars Hoogestraat 39d84c8edb updated deps 2022-01-19 23:46:57 +01:00
Lars Hoogestraat e11d46d531 use pointers 2022-01-19 23:43:56 +01:00
Lars Hoogestraat 4a58f2ed62 Merge branch 'develop' 2021-10-31 00:29:10 +02:00
Lars Hoogestraat 9ce4aa4061 updated deps 2021-10-31 00:28:40 +02:00
Lars Hoogestraat 4408371856 minor clean-up 2021-09-08 22:26:41 +02:00
Lars Hoogestraat 82a94386fd Merge branch 'develop' 2021-08-27 00:02:06 +02:00
Lars Hoogestraat 443693d17f no information when user do not exist on password reset 2021-08-27 00:01:50 +02:00
Lars Hoogestraat 86a6ab3cd2 Merge branch 'develop' 2021-07-31 14:01:14 +02:00
Lars Hoogestraat 85202d4b7d updated deps 2021-07-30 20:09:27 +02:00
Lars Hoogestraat c66cf32cd7 roll deps 2021-06-22 16:00:26 +02:00
Lars Hoogestraat ed0c47e094 updated test 2021-05-24 18:37:27 +02:00
Lars Hoogestraat aca5cd178c compatibility to session 1.3.0 2021-05-24 18:36:37 +02:00
Lars Hoogestraat f36ab23580 Merge branch 'develop' of ssh://git.hoogi.eu/snafu/go-blog into develop 2021-05-23 18:06:58 +02:00
Lars Hoogestraat d104b43b86 updated deps 2021-05-23 18:05:08 +02:00
Lars Hoogestraat 6a3905f93e use string builder instead of bytes buffer 2021-05-14 22:59:09 +02:00
Lars Hoogestraat fca29a0819 handle errors / fixed typos 2021-05-14 21:07:53 +02:00
Lars Hoogestraat 3d3352f5bd updated deps 2021-05-14 20:03:42 +02:00
Lars Hoogestraat 44ae9fcb85 updated deps 2021-05-01 21:09:56 +02:00
Lars Hoogestraat 563a00a9b9 updated deps 2021-04-28 23:38:45 +02:00
Lars Hoogestraat 42ef2bf5cc updated deps 2021-03-28 18:55:36 +02:00
Lars Hoogestraat bfa65a643b updated deps 2021-01-10 13:20:44 +01:00
Lars Hoogestraat d7a432859d comments 2021-01-10 13:14:47 +01:00
Lars Hoogestraat 21ed8c3a94 category comments 2021-01-10 12:47:18 +01:00
Lars Hoogestraat 0f1aea47ef article comments 2021-01-10 12:03:28 +01:00
Lars Hoogestraat 2a8e808ad9 clean-up 2021-01-10 11:17:59 +01:00
Lars Hoogestraat 429ac762b2 set default bcrypt rounds in crypt helper 2020-12-07 19:09:45 +01:00
Lars Hoogestraat 385c1207d3 Merge branch 'develop' 2020-12-07 18:40:17 +01:00
Lars Hoogestraat dd32851030 conform to renamed session method 2020-12-07 18:39:21 +01:00
Lars Hoogestraat c9a9d0f638 some formatting / equality replaces with error.Is 2020-12-07 17:08:30 +01:00
Lars Hoogestraat f416c8a55d use error.Is instead of equality check 2020-12-07 16:29:14 +01:00
Lars Hoogestraat f1dc6fa95a remove equals and use error.Is 2020-12-07 16:18:58 +01:00
Lars Hoogestraat eb8beeeae6 comment 2020-12-06 16:54:02 +01:00
Lars Hoogestraat 144cc03e91 clean-up middleware 2020-12-06 16:07:53 +01:00
Lars Hoogestraat aca7f56fa2 clean-up middleware 2020-12-06 16:04:12 +01:00
Lars Hoogestraat 193cb7c1c8 cleanup crypt package 2020-12-06 15:53:42 +01:00
Lars Hoogestraat 574ebba89c set go path according to go env if not defined 2020-11-30 13:43:58 +01:00
Lars Hoogestraat e89c1bdb65 fixes test 2020-11-29 18:35:31 +01:00
Lars Hoogestraat 3b5deff6a7 updated command line tools 2020-11-29 18:26:34 +01:00
Lars Hoogestraat 95834ef9f1 rename controllers in handler 2020-11-29 16:56:42 +01:00
Lars Hoogestraat b098acc6db updated dependencies 2020-11-29 16:46:23 +01:00
Lars Hoogestraat 300d629680 removed util package #3 2020-11-29 00:37:59 +01:00
Lars Hoogestraat ad9e4eaaff removed components folder 2020-11-28 16:40:45 +01:00
Lars Hoogestraat b25203d38b removed util package #2 2020-11-28 16:31:13 +01:00
Lars Hoogestraat 7ebb9706c7 removed util package 2020-11-28 16:29:55 +01:00
70 changed files with 1500 additions and 1417 deletions

3
.gitignore vendored
View File

@ -32,9 +32,6 @@ releases
.idea
.csrftoken
/clt/createUser
/clt/initDatabase
/go-blog
releases
db/

View File

@ -1,8 +1,14 @@
BINARYNAME=go-blog
TMP=tmp
DIST=release
GITHASH=$(shell git rev-parse HEAD)
BUILD_VERSION=$(shell git describe --tags)
ifndef $(GOPATH)
GOPATH=$(shell go env GOPATH)
export GOPATH
endif
RELEASE="releases"
LDFLAGS=-ldflags '-X main.BuildVersion=${BUILD_VERSION} -X main.GitHash=${GITHASH}'
@ -24,7 +30,7 @@ install:
package:
-rm -r ${TMP}
mkdir -p ${TMP}/clt
-mkdir -p releases
-mkdir -p releases/custom
cp ${GOPATH}/bin/go-blog ${TMP}/
cp ${GOPATH}/bin/create_user ${TMP}/clt
cp ${GOPATH}/bin/init_database ${TMP}/clt

View File

@ -28,7 +28,7 @@ Create the initial database (switch to folder clt/):
Change the config file to point to the correct sqlite database
~~~
sqlite_file = /path/to/your/sqlite/database
sqlite_file = /path/to/your/sqlite/database
~~~
### Create user with administration rights ###

View File

View File

@ -12,10 +12,9 @@ import (
"os"
"syscall"
"git.hoogi.eu/snafu/go-blog/components/database"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/database"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/models"
"git.hoogi.eu/snafu/go-blog/utils"
"golang.org/x/crypto/ssh/terminal"
)
@ -38,14 +37,31 @@ func main() {
fmt.Printf("create_user version %s\n", BuildVersion)
username := flag.String("username", "", "Username for the admin user ")
email := flag.String("email", "", "Email for the created user ")
displayName := flag.String("displayname", "", "Display name for the admin user ")
isAdmin := flag.Bool("admin", false, "If set a new administrator will be created; otherwise a non-admin is created")
file := flag.String("sqlite", "", "Location to the sqlite3 database file")
username := flag.String("username", "", "Username for the admin user. (required)")
email := flag.String("email", "", "Email for the created user. (required)")
displayName := flag.String("displayname", "", "Display name for the admin user. (required)")
isAdmin := flag.Bool("admin", false, "If set a new user with admin permissions will be created; otherwise a non-admin is created.")
file := flag.String("sqlite", "", "Location to the sqlite3 database file. (required)")
flag.Parse()
if *username == "" {
fmt.Println("the username (-username) must be specified")
os.Exit(1)
}
if *email == "" {
fmt.Println("the email (-email) must be specified")
os.Exit(1)
}
if *displayName == "" {
fmt.Println("the display name (-displayname) must be specified")
os.Exit(1)
}
if *file == "" {
fmt.Println("the argument -sqlite is empty. Please specify the location of the sqlite3 database file")
os.Exit(1)
}
if flag.Parsed() {
initUser := createUserFlag{
username: *username,
@ -55,16 +71,13 @@ func main() {
sqlite: *file,
}
if err := initUser.validate(); err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Printf("Password: ")
pw, err := terminal.ReadPassword(int(syscall.Stdin))
fmt.Println("")
if err != nil {
fmt.Printf("could not read password %v", err)
fmt.Printf("could not read password %v\n", err)
os.Exit(1)
}
@ -85,28 +98,7 @@ func main() {
}
}
func (userFlags createUserFlag) validate() error {
if utils.TrimmedStringIsEmpty(userFlags.username) {
return fmt.Errorf("the username (-username) must be specified")
}
if utils.TrimmedStringIsEmpty(userFlags.email) {
return fmt.Errorf("the email (-email) must be specified")
}
if utils.TrimmedStringIsEmpty(userFlags.displayName) {
return fmt.Errorf("the display name (-displayname) must be specified")
}
if utils.TrimmedStringIsEmpty(userFlags.sqlite) {
return fmt.Errorf("the argument -sqlite is empty. Please specify the location of the sqlite3 database file")
}
return nil
}
func (userFlags createUserFlag) CreateUser() error {
var userService models.UserService
dbConfig := database.SQLiteConfig{
File: userFlags.sqlite,
}
@ -117,8 +109,8 @@ func (userFlags createUserFlag) CreateUser() error {
return err
}
userService = models.UserService{
Datasource: models.SQLiteUserDatasource{
userService := &models.UserService{
Datasource: &models.SQLiteUserDatasource{
SQLConn: db,
},
}

View File

@ -12,8 +12,8 @@ import (
"os"
"strings"
"git.hoogi.eu/snafu/go-blog/components/database"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/database"
"git.hoogi.eu/snafu/go-blog/logger"
)
var (

76
crypt/crypt.go Normal file
View File

@ -0,0 +1,76 @@
// Copyright 2018 Lars Hoogestraat
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// Package crypt provides utilities for creating hashes, random strings, password encryption
package crypt
import (
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"io"
"math/big"
"golang.org/x/crypto/bcrypt"
)
const bcryptRounds = 12
var (
// AlphaUpper all upper alphas chars
AlphaUpper = RandomSource("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
// AlphaLower all lowers alphas chars
AlphaLower = RandomSource("abcdefghijklmnopqrstuvwxyz")
// AlphaUpperLower all upper and lowers aplhas chars
AlphaUpperLower = RandomSource("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
// AlphaUpperLowerNumeric all upper lowers alphas and numerics
AlphaUpperLowerNumeric = RandomSource("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz")
// AlphaUpperLowerNumericSpecial all upper lowers alphas, numerics and special chars
AlphaUpperLowerNumericSpecial = RandomSource("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456890" +
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~")
)
// RandomSource string containing which characters should be considered when generating random sequences
type RandomSource string
// RandomSequence returns random character with given length;
func (r RandomSource) RandomSequence(length int) []byte {
result := make([]byte, length)
for i := 0; i < length; i++ {
char, _ := rand.Int(rand.Reader, big.NewInt(int64(len(r))))
result[i] = r[int(char.Int64())]
}
return result
}
// RandomSecureKey returns random character with given length
func RandomSecureKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}
// CryptPassword hashes a password with bcrypt and a given cost
func CryptPassword(password []byte) ([]byte, error) {
return bcrypt.GenerateFromPassword(password, bcryptRounds)
}
// GenerateSalt generates a random salt with alphanumerics and some special characters
func GenerateSalt() []byte {
return AlphaUpperLowerNumericSpecial.RandomSequence(32)
}
// RandomHash returns a random SHA-512 hash
func RandomHash(length int) string {
hash := sha512.New()
hash.Write(RandomSecureKey(length))
return hex.EncodeToString(hash.Sum(nil))
}

View File

@ -10,23 +10,17 @@ import (
_ "github.com/mattn/go-sqlite3"
)
//SQLiteConfig represents sqlite configuration type
// SQLiteConfig represents sqlite configuration type
type SQLiteConfig struct {
File string
}
//Open receives handle for sqlite database, returns an error if connection failed
// Open receives handle for sqlite database, returns an error if connection failed
func (d SQLiteConfig) Open() (*sql.DB, error) {
db, err := sql.Open("sqlite3", d.File)
if err != nil {
return nil, err
}
return db, nil
return sql.Open("sqlite3", d.File)
}
//InitTables creates the tables
// InitTables creates the tables
func InitTables(db *sql.DB) error {
if _, err := db.Exec("CREATE TABLE user " +
"(" +
@ -148,5 +142,6 @@ func InitTables(db *sql.DB) error {
");"); err != nil {
return err
}
return nil
}

View File

@ -60,7 +60,7 @@ application_overwrite_default_css = false
# log levels: debug|info|warn|error|fatal|panic
log_level = Error
log_file = /var/log/go-blog/error.log
log_access = false
log_access = true
log_access_file = /var/log/go-blog/access.log
########### BLOG SETTINGS ###########

38
go.mod
View File

@ -1,20 +1,32 @@
module git.hoogi.eu/snafu/go-blog
go 1.15
go 1.21
toolchain go1.21.5
require (
git.hoogi.eu/snafu/cfg v1.0.6
git.hoogi.eu/snafu/session v1.1.2
github.com/gorilla/csrf v1.7.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
git.hoogi.eu/snafu/session v1.3.0
github.com/gorilla/csrf v1.7.2
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
github.com/justinas/alice v1.2.0
github.com/mattn/go-sqlite3 v1.14.3
github.com/microcosm-cc/bluemonday v1.0.4
github.com/russross/blackfriday/v2 v2.0.1
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.6.0
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect
github.com/mattn/go-sqlite3 v1.14.19
github.com/microcosm-cc/bluemonday v1.0.26
github.com/russross/blackfriday/v2 v2.1.0
github.com/sirupsen/logrus v1.9.3
golang.org/x/crypto v0.17.0
)
require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.7.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

192
go.sum
View File

@ -1,58 +1,182 @@
git.hoogi.eu/snafu/cfg v1.0.6 h1:O34hYFqjwfnMjEwB4M8GaQQmBtf3H+AA0RFHb7PoMNs=
git.hoogi.eu/snafu/cfg v1.0.6/go.mod h1:LQolv8bqH8ZPz7h9PSVswdBXj1BIM+kW79AVS/jpE7A=
git.hoogi.eu/snafu/session v1.1.2 h1:MclTbSqD/9JodRUqFc4OwyJGk5AtonqB6BaG5RRJHf8=
git.hoogi.eu/snafu/session v1.1.2/go.mod h1:kgRDrnHcKc9H18G9533BXy6qO+81eBf6e9gkUzBMDuA=
git.hoogi.eu/snafu/session v1.3.0 h1:CzJQG7rseuerwBcLwxoJDtvWjXNP13bnWE90TsuTAls=
git.hoogi.eu/snafu/session v1.3.0/go.mod h1:kgRDrnHcKc9H18G9533BXy6qO+81eBf6e9gkUzBMDuA=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
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/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o=
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y=
github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk=
github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/microcosm-cc/bluemonday v1.0.15 h1:J4uN+qPng9rvkBZBoBb8YGR+ijuklIMpSOZZLjYpbeY=
github.com/microcosm-cc/bluemonday v1.0.15/go.mod h1:ZLvAzeakRwrGnzQEvstVzVt3ZpqOF2+sdFr0Om+ce30=
github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc=
github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/microcosm-cc/bluemonday v1.0.18 h1:6HcxvXDAi3ARt3slx6nTesbvorIc3QeTzBNRvWktHBo=
github.com/microcosm-cc/bluemonday v1.0.18/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/microcosm-cc/bluemonday v1.0.19 h1:OI7hoF5FY4pFz2VA//RN8TfM0YJ2dJcl4P4APrCWy6c=
github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE=
github.com/microcosm-cc/bluemonday v1.0.20 h1:flpzsq4KU3QIYAYGV/szUat7H+GPOXR0B2JU5A1Wp8Y=
github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50=
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/microcosm-cc/bluemonday v1.0.22 h1:p2tT7RNzRdCi0qmwxG+HbqD6ILkmwter1ZwVZn1oTxA=
github.com/microcosm-cc/bluemonday v1.0.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210716203947-853a461950ff h1:j2EK/QoxYNBsXI4R7fQkkRUk8y6wnOBI+6hgPdP/6Ds=
golang.org/x/net v0.0.0-20210716203947-853a461950ff/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs=
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462 h1:UreQrH7DbFXSi9ZFox6FNT3WBooWmdANpU+IfkT1T4I=
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b h1:6e93nYa3hNqAvLr0pD4PN1fFS+gKzp2zAXqrnTCstqU=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211030160813-b3129d9d1021 h1:giLT+HuUP/gXYrG2Plg9WTjj4qhfgaW424ZIFog3rlk=
golang.org/x/sys v0.0.0-20211030160813-b3129d9d1021/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220730100132-1609e554cd39 h1:aNCnH+Fiqs7ZDTFH6oEFjIfbX2HvgQXJ6uQuUbTobjk=
golang.org/x/sys v0.0.0-20220730100132-1609e554cd39/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,4 +1,4 @@
package controllers
package handler
import (
"bytes"
@ -8,13 +8,13 @@ import (
"net/http"
"time"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/middleware"
"git.hoogi.eu/snafu/go-blog/models"
)
//AdminProfileHandler returns page for updating the profile of the currently logged-in user
// AdminProfileHandler returns the page for updating the profile of the currently logged-in user
func AdminProfileHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
user, _ := middleware.User(r)
@ -27,7 +27,7 @@ func AdminProfileHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *h
}
}
//AdminProfilePostHandler handles the updating of the user profile which is currently logged in
// AdminProfilePostHandler handles the updating of the user profile which is currently logged in
func AdminProfilePostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
ctxUser, _ := middleware.User(r)
ctxUser.PlainPassword = []byte(r.FormValue("current_password"))
@ -57,7 +57,7 @@ func AdminProfilePostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
if len(u.PlainPassword) > 0 {
changePassword = true
// Password change
u.PlainPassword = []byte(r.FormValue("password"))
if !bytes.Equal(u.PlainPassword, []byte(r.FormValue("retyped_password"))) {
@ -81,11 +81,11 @@ func AdminProfilePostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
session.SetValue("userid", u.ID)
sids := ctx.SessionService.SessionProvider.SessionIDsFromValues("userid", u.ID)
sessions := ctx.SessionService.SessionProvider.FindByValue("userid", u.ID)
for _, sid := range sids {
if sid != session.SessionID() {
ctx.SessionService.SessionProvider.Remove(sid)
for _, sid := range sessions {
if sid.SessionID() != session.SessionID() {
ctx.SessionService.SessionProvider.Remove(sid.SessionID())
}
}
}
@ -111,7 +111,7 @@ func AdminProfilePostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
//ActivateAccountHandler shows the form to activate an account.
// ActivateAccountHandler shows the form to activate an account
func ActivateAccountHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
hash := getVar(r, "hash")
@ -139,9 +139,9 @@ func ActivateAccountHandler(ctx *middleware.AppContext, w http.ResponseWriter, r
}
}
//ActivateAccountPostHandler activates an user account
// ActivateAccountPostHandler activates an user account
func ActivateAccountPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
// Delete any cookies if an user is logged in
// Delete cookie if the user is logged in
ctx.SessionService.Remove(w, r)
password := r.FormValue("password")
@ -196,11 +196,11 @@ func ActivateAccountPostHandler(ctx *middleware.AppContext, w http.ResponseWrite
return &middleware.Template{
RedirectPath: "admin",
SuccessMsg: "The account was successfully activated. You can now log in.",
SuccessMsg: "The account was successfully activated. You can now login.",
}
}
//ResetPasswordHandler returns the form for resetting the password
// ResetPasswordHandler returns the form to reset the password
func ResetPasswordHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
hash := getVar(r, "hash")
@ -227,11 +227,11 @@ func ResetPasswordHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *
}
}
//ResetPasswordPostHandler handles the resetting of the password
// ResetPasswordPostHandler handles a password reset
func ResetPasswordPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
password := r.FormValue("password")
repassword := r.FormValue("password_repeat")
hash := getVar(r, "hash")
password := r.FormValue("password")
password2 := r.FormValue("password_repeat")
t, err := ctx.TokenService.Get(hash, models.PasswordReset, time.Duration(1)*time.Hour)
@ -251,7 +251,7 @@ func ResetPasswordPostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
if password != repassword {
if password != password2 {
return &middleware.Template{
Name: tplAdminResetPassword,
Err: httperror.New(http.StatusUnprocessableEntity, "The passwords entered do not match.", errors.New("the password entered did not match")),
@ -277,31 +277,43 @@ func ResetPasswordPostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
ctx.Mailer.SendPasswordChangeConfirmation(u)
if !u.Active {
logger.Log.Warnf("password reset for user '%s' was successful, but user is deactivated", u.Email)
return &middleware.Template{
Name: tplAdminResetPassword,
WarnMsg: "Your password reset was successful, but your account is deactivated.",
}
}
return &middleware.Template{
RedirectPath: "admin",
SuccessMsg: "Your password reset was successful.",
}
}
//ForgotPasswordHandler returns the form for the reset password form
// ForgotPasswordHandler returns the form for the password reset
func ForgotPasswordHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
return &middleware.Template{
Name: tplAdminForgotPassword,
}
}
//ForgotPasswordPostHandler handles the processing of the reset password function
// ForgotPasswordPostHandler handles the processing for the password reset
func ForgotPasswordPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
email := r.FormValue("email")
u, err := ctx.UserService.GetByMail(email)
if err != nil {
if httperror.Equals(err, sql.ErrNoRows) {
logger.Log.Error(err)
return &middleware.Template{
RedirectPath: "admin",
SuccessMsg: "An email with password reset instructions is on the way.",
var e *httperror.Error
if errors.As(err, &e) {
if errors.Is(e.Err, sql.ErrNoRows) {
logger.Log.Error(err)
return &middleware.Template{
Name: tplAdminForgotPassword,
SuccessMsg: fmt.Sprintf("An email to '%s' with password reset instructions is on the way.", email),
}
}
} else {
return &middleware.Template{
@ -316,31 +328,16 @@ func ForgotPasswordPostHandler(ctx *middleware.AppContext, w http.ResponseWriter
}
}
if !u.Active {
return &middleware.Template{
Name: tplAdminForgotPassword,
Err: httperror.New(http.StatusUnauthorized, "Your account is deactivated.", err),
Data: map[string]interface{}{
"user": models.User{
Email: email,
},
},
}
}
t := &models.Token{
Author: u,
Type: models.PasswordReset,
}
err = ctx.TokenService.RateLimit(u.ID, models.PasswordReset)
if err != nil {
if err = ctx.TokenService.RateLimit(u.ID, models.PasswordReset); err != nil {
logger.Log.Error(err)
}
err = ctx.TokenService.Create(t)
if err != nil {
if err = ctx.TokenService.Create(t); err != nil {
return &middleware.Template{
Name: tplAdminForgotPassword,
Err: err,
@ -350,7 +347,7 @@ func ForgotPasswordPostHandler(ctx *middleware.AppContext, w http.ResponseWriter
ctx.Mailer.SendPasswordResetLink(u, t)
return &middleware.Template{
RedirectPath: "admin",
SuccessMsg: "An email with password reset instructions is on the way.",
Name: tplAdminForgotPassword,
SuccessMsg: fmt.Sprintf("An email to '%s' with password reset instructions is on the way.", email),
}
}

View File

@ -1,11 +1,11 @@
package controllers_test
package handler_test
import (
"net/http/httptest"
"net/url"
"testing"
"git.hoogi.eu/snafu/go-blog/controllers"
"git.hoogi.eu/snafu/go-blog/handler"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -94,7 +94,7 @@ func doAdminProfileRequest(user reqUser, u *models.User, currentPassword string)
rw := httptest.NewRecorder()
re := r.buildRequest()
tpl := controllers.AdminProfilePostHandler(ctx, rw, re)
tpl := handler.AdminProfilePostHandler(ctx, rw, re)
if tpl.Err != nil {
return tpl.Err
@ -122,7 +122,7 @@ func doActivateAccountRequest(user reqUser, password, passwordRepeat, hash strin
}
rw := httptest.NewRecorder()
tpl := controllers.ActivateAccountPostHandler(ctx, rw, r.buildRequest())
tpl := handler.ActivateAccountPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err
@ -150,7 +150,7 @@ func doResetPasswordRequest(user reqUser, password, passwordRepeat, hash string)
}
rw := httptest.NewRecorder()
tpl := controllers.AdminSiteEditPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminSiteEditPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package controllers
package handler
import (
"database/sql"
@ -10,20 +10,21 @@ import (
"net/http"
"time"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/middleware"
"git.hoogi.eu/snafu/go-blog/models"
"git.hoogi.eu/snafu/go-blog/utils"
)
//GetArticleHandler returns a specific article
//Parameters in the url form 2016/3/my-headline are used for obtaining the article
// GetArticleHandler returns a specific article
// URL Parameters are in the form /year/month/slug e.g. 2016/3/my-
func GetArticleHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
year := getVar(r, "year")
month := getVar(r, "month")
slug := getVar(r, "slug")
headline := getVar(r, "slug")
a, err := ctx.ArticleService.GetBySlug(utils.AppendString(year, "/", month, "/", slug), nil, models.OnlyPublished)
slug := year + "/" + month + "/" + headline
a, err := ctx.ArticleService.GetBySlug(slug, nil, models.OnlyPublished)
if err != nil {
return &middleware.Template{
@ -50,8 +51,7 @@ func GetArticleHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *htt
}}
}
//GetArticleHandler returns a specific article
//Parameters in the url form 2016/03/my-headline are used for obtaining the article
// GetArticleByIDHandler returns a specific article by the ID
func GetArticleByIDHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
id, err := parseInt(getVar(r, "articleID"))
@ -89,7 +89,7 @@ func GetArticleByIDHandler(ctx *middleware.AppContext, w http.ResponseWriter, r
}}
}
//ListArticlesHandler returns the template which contains all published articles
// ListArticlesHandler returns all published articles
func ListArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
page := getPageParam(r)
@ -141,7 +141,7 @@ func ListArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *h
}
}
//ListArticlesHandler returns the template which contains all published articles
// ListArticlesCategoryHandler returns all published articles in a category
func ListArticlesCategoryHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
page := getPageParam(r)
@ -206,7 +206,39 @@ func ListArticlesCategoryHandler(ctx *middleware.AppContext, w http.ResponseWrit
}
}
//IndexArticlesCategoryHandler returns the template information for the index page grouped by categories
// IndexArticlesHandler returns articles for the index page
func IndexArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
a, err := ctx.ArticleService.Index(nil, nil, nil, models.OnlyPublished)
if err != nil {
return &middleware.Template{
Name: tplIndexArticles,
Active: "index",
Err: err,
}
}
c, err := ctx.CategoryService.List(models.CategoriesWithPublishedArticles)
if err != nil {
return &middleware.Template{
Name: tplIndexArticles,
Active: "index",
Err: err,
}
}
return &middleware.Template{
Name: tplIndexArticles,
Active: "index",
Data: map[string]interface{}{
"articles": a,
"categories": c,
},
}
}
// IndexArticlesCategoryHandler returns articles for the index page grouped by categories
func IndexArticlesCategoryHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
cs, err := ctx.CategoryService.List(models.CategoriesWithPublishedArticles)
@ -256,40 +288,7 @@ func IndexArticlesCategoryHandler(ctx *middleware.AppContext, w http.ResponseWri
}
}
//IndexArticlesHandler returns the template information for the index page
func IndexArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
a, err := ctx.ArticleService.Index(nil, nil, nil, models.OnlyPublished)
if err != nil {
return &middleware.Template{
Name: tplIndexArticles,
Active: "index",
Err: err,
}
}
c, err := ctx.CategoryService.List(models.CategoriesWithPublishedArticles)
if err != nil {
return &middleware.Template{
Name: tplIndexArticles,
Active: "index",
Err: err,
}
}
return &middleware.Template{
Name: tplIndexArticles,
Active: "index",
Data: map[string]interface{}{
"articles": a,
"categories": c,
},
}
}
//GetArticleHandler returns a specific article
//Parameters in the url form 2016/03/my-headline are used for obtaining the article
// RSSFeed returns XML list of published articles for the RSS feed
func RSSFeed(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) (*models.XMLData, error) {
p := &models.Pagination{
Limit: ctx.ConfigService.RSSFeedItems,
@ -307,7 +306,7 @@ func RSSFeed(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request)
}, nil
}
//AdminListArticlesHandler returns all articles, also not yet published articles will be shown
// AdminListArticlesHandler returns all articles, also not yet published articles
func AdminListArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -346,8 +345,7 @@ func AdminListArticlesHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}}
}
//AdminShowArticleByIDHandler returns a specific article, renders it on the front page
//used for the preview
// AdminPreviewArticleByIDHandler returns a specific article, renders it on the front page used for the preview
func AdminPreviewArticleByIDHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -391,7 +389,7 @@ func AdminPreviewArticleByIDHandler(ctx *middleware.AppContext, w http.ResponseW
}}
}
// AdminArticleNewHandler returns the template which shows the form to create a new article
// AdminArticleNewHandler returns the form to create a new article
func AdminArticleNewHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
c, err := ctx.CategoryService.List(models.AllCategories)
@ -457,7 +455,7 @@ func AdminArticleNewPostHandler(ctx *middleware.AppContext, w http.ResponseWrite
}
}
//AdminArticleEditHandler shows the form for changing an article
// AdminArticleEditHandler shows the form for changing an article
func AdminArticleEditHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -498,7 +496,7 @@ func AdminArticleEditHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
//AdminArticleEditPostHandler handles the update of an article
// AdminArticleEditPostHandler handles the update of an article
func AdminArticleEditPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -555,7 +553,7 @@ func AdminArticleEditPostHandler(ctx *middleware.AppContext, w http.ResponseWrit
}
}
//AdminArticlePublishHandler returns the action template which asks the user if the article should be published / unpublished
// AdminArticlePublishHandler returns the action template which asks the user if the article should be published / unpublished
func AdminArticlePublishHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -573,6 +571,14 @@ func AdminArticlePublishHandler(ctx *middleware.AppContext, w http.ResponseWrite
a, err := ctx.ArticleService.GetByID(id, u, models.All)
if err != nil {
return &middleware.Template{
Name: tplAdminArticles,
Err: err,
Active: "articles",
}
}
var action models.Action
if a.Published {
@ -602,7 +608,7 @@ func AdminArticlePublishHandler(ctx *middleware.AppContext, w http.ResponseWrite
}
}
// AdminArticlePublishPostHandler publishes or "depublishes" an article
// AdminArticlePublishPostHandler publish or unpublish an article
func AdminArticlePublishPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -631,7 +637,7 @@ func AdminArticlePublishPostHandler(ctx *middleware.AppContext, w http.ResponseW
}
}
//AdminArticleDeleteHandler returns the action template which asks the user if the article should be removed
// AdminArticleDeleteHandler returns the action template which asks the user if the article should be removed
func AdminArticleDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -674,7 +680,7 @@ func AdminArticleDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter
}
}
//AdminArticleDeletePostHandler handles the removing of an article
// AdminArticleDeletePostHandler handles the removing of an article
func AdminArticleDeletePostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package controllers_test
package handler_test
import (
"fmt"
@ -13,7 +13,7 @@ import (
"strconv"
"testing"
"git.hoogi.eu/snafu/go-blog/controllers"
"git.hoogi.eu/snafu/go-blog/handler"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -143,7 +143,7 @@ func doGetArticleBySlugRequest(user reqUser, article *models.Article) (*models.A
}
rw := httptest.NewRecorder()
tpl := controllers.GetArticleHandler(ctx, rw, r.buildRequest())
tpl := handler.GetArticleHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -166,7 +166,7 @@ func doGetArticleByIDRequest(user reqUser, articleID int) (*models.Article, erro
}
rw := httptest.NewRecorder()
tpl := controllers.GetArticleByIDHandler(ctx, rw, r.buildRequest())
tpl := handler.GetArticleByIDHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -196,7 +196,7 @@ func doAdminEditArticleRequest(user reqUser, articleID int, article *models.Arti
rw := httptest.NewRecorder()
tpl := controllers.AdminArticleEditPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminArticleEditPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err
@ -222,7 +222,7 @@ func doAdminCreateArticleRequest(user reqUser, article *models.Article) (int, er
}
rw := httptest.NewRecorder()
tpl := controllers.AdminArticleNewPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminArticleNewPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return 0, tpl.Err
@ -249,7 +249,7 @@ func doAdminGetArticleByIDRequest(user reqUser, articleID int) (*models.Article,
}
rw := httptest.NewRecorder()
tpl := controllers.AdminPreviewArticleByIDHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminPreviewArticleByIDHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -266,7 +266,7 @@ func doAdminListArticleRequest(user reqUser) ([]models.Article, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminListArticlesHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminListArticlesHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -289,7 +289,7 @@ func doAdminPublishArticleRequest(user reqUser, articleID int) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminArticlePublishPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminArticlePublishPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err
@ -312,7 +312,7 @@ func doAdminRemoveArticleRequest(user reqUser, articleID int) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminArticleDeletePostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminArticleDeletePostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err

View File

@ -2,17 +2,18 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package controllers
package handler
import (
"fmt"
"net/http"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/middleware"
"git.hoogi.eu/snafu/go-blog/models"
)
// AdminListCategoriesHandler returns a list of all categories
func AdminListCategoriesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
c, err := ctx.CategoryService.List(models.AllCategories)
@ -31,6 +32,7 @@ func AdminListCategoriesHandler(ctx *middleware.AppContext, w http.ResponseWrite
}}
}
// AdminGetCategoryHandler get category by the ID
func AdminGetCategoryHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
reqVar := getVar(r, "categoryID")
id, err := parseInt(reqVar)
@ -59,7 +61,7 @@ func AdminGetCategoryHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}}
}
// AdminCategoryNewHandler returns the template which shows the form to create a new article
// AdminCategoryNewHandler returns the form to create a new category
func AdminCategoryNewHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
return &middleware.Template{
Active: "categories",
@ -67,7 +69,7 @@ func AdminCategoryNewHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
// AdminCategoryNewPostHandler handles the creation of a new article
// AdminCategoryNewPostHandler handles the creation of a new category
func AdminCategoryNewPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -99,7 +101,7 @@ func AdminCategoryNewPostHandler(ctx *middleware.AppContext, w http.ResponseWrit
}
}
//AdminCategoryEditHandler shows the form for changing an article
// AdminCategoryEditHandler shows the form to change a category
func AdminCategoryEditHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
id, err := parseInt(getVar(r, "categoryID"))
@ -128,7 +130,7 @@ func AdminCategoryEditHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
//AdminArticleEditPostHandler handles the update of an article
// AdminCategoryEditPostHandler handles the update of a category
func AdminCategoryEditPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -168,7 +170,7 @@ func AdminCategoryEditPostHandler(ctx *middleware.AppContext, w http.ResponseWri
}
}
//AdminArticleDeleteHandler returns the action template which asks the user if the article should be removed
// AdminCategoryDeleteHandler returns the action which asks the user if the category should be removed
func AdminCategoryDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
reqVar := getVar(r, "categoryID")
@ -209,7 +211,7 @@ func AdminCategoryDeleteHandler(ctx *middleware.AppContext, w http.ResponseWrite
}
}
//AdminArticleDeletePostHandler handles the removing of an article
// AdminCategoryDeletePostHandler handles the removing of a category
func AdminCategoryDeletePostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
reqVar := getVar(r, "categoryID")

View File

@ -1,4 +1,4 @@
package controllers_test
package handler_test
import (
"net/http/httptest"
@ -6,7 +6,7 @@ import (
"strconv"
"testing"
"git.hoogi.eu/snafu/go-blog/controllers"
"git.hoogi.eu/snafu/go-blog/handler"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -82,7 +82,7 @@ func doAdminGetCategoryRequest(user reqUser, categoryID int) (*models.Category,
}
rw := httptest.NewRecorder()
tpl := controllers.AdminGetCategoryHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminGetCategoryHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -99,7 +99,7 @@ func doAdminListCategoriesRequest(user reqUser) ([]models.Category, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminListCategoriesHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminListCategoriesHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -119,7 +119,7 @@ func doAdminCategoryNewRequest(user reqUser, c *models.Category) (int, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminCategoryNewPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminCategoryNewPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return -1, tpl.Err
@ -144,7 +144,7 @@ func doAdminCategoryEditRequest(user reqUser, c *models.Category) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminCategoryEditPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminCategoryEditPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err
@ -166,7 +166,7 @@ func doAdminDeleteCategoryRequest(user reqUser, categoryID int) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminCategoryDeletePostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminCategoryDeletePostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err

View File

@ -1,4 +1,4 @@
package controllers
package handler
const (
tplArticle = "front/article"

View File

@ -1,4 +1,4 @@
package controllers
package handler
import (
"errors"
@ -9,8 +9,8 @@ import (
"path/filepath"
"syscall"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/middleware"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -19,7 +19,7 @@ type FileHandler struct {
Context *middleware.AppContext
}
//FileGetHandler serves the file based on the url filename
// FileGetHandler serves the file based on the unique filename
func (fh FileHandler) FileGetHandler(w http.ResponseWriter, r *http.Request) {
rv := getVar(r, "uniquename")
@ -55,12 +55,19 @@ func (fh FileHandler) FileGetHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
}
defer rf.Close()
defer func(rf *os.File) {
err := rf.Close()
if err != nil {
logger.Log.Errorf("error while closing the file %v", err)
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
}
}(rf)
http.ServeContent(w, r, loc, f.LastModified, rf)
}
//AdminListFilesHandler returns the template which lists all uploaded files belonging to a user, admins will see all files
// AdminListFilesHandler returns the template which lists all uploaded files belonging to a user, admins will see all files
func AdminListFilesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -102,7 +109,7 @@ func AdminListFilesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r
}}
}
//AdminUploadFileHandler returns the form for uploading a file
// AdminUploadFileHandler returns the form for uploading a file
func AdminUploadFileHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
return &middleware.Template{
Name: tplAdminFileUpload,
@ -110,6 +117,7 @@ func AdminUploadFileHandler(ctx *middleware.AppContext, w http.ResponseWriter, r
}
}
// AdminToggleInlineFilePostHandler toggles the flag if the file should be inline or downloaded (toggles Content-Disposition header)
func AdminToggleInlineFilePostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -173,6 +181,7 @@ func AdminUploadFilePostHandler(ctx *middleware.AppContext, w http.ResponseWrite
}
}
// AdminUploadJSONFilePostHandler
func AdminUploadJSONFilePostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) (*models.JSONData, error) {
file, err := parseFileField(ctx, w, r)
@ -197,7 +206,7 @@ func AdminUploadJSONFilePostHandler(ctx *middleware.AppContext, w http.ResponseW
return json, nil
}
//AdminUploadDeleteHandler returns the action template which asks the user if the file should be removed
// AdminUploadDeleteHandler returns the action template which asks the user if the file should be removed
func AdminUploadDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -239,7 +248,7 @@ func AdminUploadDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
//AdminUploadDeletePostHandler removes a file
// AdminUploadDeletePostHandler removes a file
func AdminUploadDeletePostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)

View File

@ -1,4 +1,4 @@
package controllers_test
package handler_test
import (
"fmt"
@ -7,7 +7,7 @@ import (
"strconv"
"testing"
"git.hoogi.eu/snafu/go-blog/controllers"
"git.hoogi.eu/snafu/go-blog/handler"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -63,7 +63,7 @@ func doAdminListFilesRequest(user reqUser) ([]models.File, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminListFilesHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminListFilesHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -87,7 +87,7 @@ func doAdminGetFileRequest(user reqUser, uniquename string) (*httptest.ResponseR
rw := httptest.NewRecorder()
fh := controllers.FileHandler{
fh := handler.FileHandler{
Context: ctx,
}
@ -116,7 +116,7 @@ func doAdminUploadFileRequest(user reqUser, file string) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminUploadFilePostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminUploadFilePostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err
@ -139,7 +139,7 @@ func doAdminFileDeleteRequest(user reqUser, fileID int) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminUploadDeletePostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminUploadDeletePostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err

View File

@ -2,13 +2,13 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package controllers
package handler
import (
"net/http"
"strconv"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/logger"
"github.com/gorilla/mux"
)

View File

@ -2,17 +2,19 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package controllers
package handler
import (
"net/http"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/middleware"
"git.hoogi.eu/snafu/go-blog/models"
)
// LoginHandler shows the login form;
// if the user is already logged in the user will be redirected to the administration page of aricles
// if the user is already logged in the user will be redirected to the administration articles page
func LoginHandler(ctx *middleware.AppContext, rw http.ResponseWriter, r *http.Request) *middleware.Template {
_, err := ctx.SessionService.Get(rw, r)
@ -29,7 +31,7 @@ func LoginHandler(ctx *middleware.AppContext, rw http.ResponseWriter, r *http.Re
}
// LoginPostHandler receives the login information from the form; checks the login and
// starts a session for the user. The sesion will be stored in a cookie
// starts a session for the user. The session will be stored in a cookie
func LoginPostHandler(ctx *middleware.AppContext, rw http.ResponseWriter, r *http.Request) *middleware.Template {
if err := r.ParseForm(); err != nil {
return &middleware.Template{
@ -75,7 +77,9 @@ func LoginPostHandler(ctx *middleware.AppContext, rw http.ResponseWriter, r *htt
// LogoutHandler logs the user out by removing the cookie and removing the session from the session store
func LogoutHandler(ctx *middleware.AppContext, rw http.ResponseWriter, r *http.Request) *middleware.Template {
ctx.SessionService.Remove(rw, r)
if err := ctx.SessionService.Remove(rw, r); err != nil {
logger.Log.Infof("LogoutHandler: unable to remove session, err: %v", err)
}
return &middleware.Template{
RedirectPath: "admin",
@ -85,9 +89,7 @@ func LogoutHandler(ctx *middleware.AppContext, rw http.ResponseWriter, r *http.R
// KeepAliveSessionHandler keeps a session alive.
func KeepAliveSessionHandler(ctx *middleware.AppContext, rw http.ResponseWriter, r *http.Request) (*models.JSONData, error) {
_, err := ctx.SessionService.Get(rw, r)
if err != nil {
if _, err := ctx.SessionService.Get(rw, r); err != nil {
return nil, err
}

View File

@ -1,4 +1,4 @@
package controllers_test
package handler_test
import (
"fmt"
@ -6,7 +6,7 @@ import (
"net/url"
"testing"
"git.hoogi.eu/snafu/go-blog/controllers"
"git.hoogi.eu/snafu/go-blog/handler"
)
func TestLogin(t *testing.T) {
@ -71,7 +71,7 @@ func doLoginRequest(user reqUser, login, password string) (responseWrapper, erro
}
rr := httptest.NewRecorder()
tpl := controllers.LoginPostHandler(ctx, rr, r.buildRequest())
tpl := handler.LoginPostHandler(ctx, rr, r.buildRequest())
if tpl.Err != nil {
return responseWrapper{response: rr, template: tpl}, tpl.Err

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package controllers
package handler
import (
"database/sql"
@ -10,12 +10,12 @@ import (
"fmt"
"net/http"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/middleware"
"git.hoogi.eu/snafu/go-blog/models"
)
//GetSiteHandler returns the site template - only published sites are considered
// GetSiteHandler returns the published sites
func GetSiteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
site, err := ctx.SiteService.GetByLink(getVar(r, "site"), models.OnlyPublished)
@ -40,7 +40,7 @@ func GetSiteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.R
}
}
//AdminGetSiteHandler returns the template containing the sites
// AdminGetSiteHandler returns a specific site by the ID
func AdminGetSiteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
reqVar := getVar(r, "siteID")
@ -76,7 +76,7 @@ func AdminGetSiteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *h
}
}
//AdminSitesHandler returns the template containing the sites overview in the administration
// AdminSitesHandler returns all sites
func AdminSitesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
page := getPageParam(r)
@ -121,7 +121,7 @@ func AdminSitesHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *htt
}
}
//AdminSiteNewHandler returns the template for adding a new site
// AdminSiteNewHandler returns the form for adding a new site
func AdminSiteNewHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
return &middleware.Template{
Name: tplAdminSiteNew,
@ -129,8 +129,8 @@ func AdminSiteNewHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *h
}
}
//AdminSiteNewPostHandler receives the form values and creating the site; on success the user is redirected with a success message
//to the site overview
// AdminSiteNewPostHandler receives the form values and creating the site; on success the user is redirected with a success message
// to the site overview
func AdminSiteNewPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
user, _ := middleware.User(r)
@ -169,7 +169,7 @@ func AdminSiteNewPostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
//AdminSiteEditHandler returns the template for editing an existing site
// AdminSiteEditHandler returns the form for changing an existing site
func AdminSiteEditHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
siteID, err := parseInt(getVar(r, "siteID"))
@ -198,8 +198,8 @@ func AdminSiteEditHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *
}
}
//AdminSiteEditPostHandler receives the form values and updates the site; on success the user is redirected with a success message
//to the site overview
// AdminSiteEditPostHandler receives the form values and updates the site; on success the user is redirected with a success message
// to the site overview
func AdminSiteEditPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u, _ := middleware.User(r)
@ -244,7 +244,7 @@ func AdminSiteEditPostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
//AdminSiteOrderHandler moves the site with site id down or up
// AdminSiteOrderHandler moves the site with site ID down or up
func AdminSiteOrderHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
siteID, err := parseInt(getVar(r, "siteID"))
@ -286,7 +286,7 @@ func AdminSiteOrderHandler(ctx *middleware.AppContext, w http.ResponseWriter, r
}
}
//AdminSitePublishHandler returns the action template which asks the user if the site should be published / unpublished
// AdminSitePublishHandler returns the action template which asks the user if the site should be published / unpublished
func AdminSitePublishHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
reqVar := getVar(r, "siteID")
@ -338,7 +338,7 @@ func AdminSitePublishHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
//AdminSitePublishPostHandler handles the un-/publishing of a site
// AdminSitePublishPostHandler handles the un-/publishing of a site
func AdminSitePublishPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
reqVar := getVar(r, "siteID")
@ -367,7 +367,7 @@ func AdminSitePublishPostHandler(ctx *middleware.AppContext, w http.ResponseWrit
}
}
//AdminSiteDeleteHandler returns the action template which asks the user if the site should be removed
// AdminSiteDeleteHandler returns the action which asks the user if the site should be removed
func AdminSiteDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
reqVar := getVar(r, "siteID")
@ -408,7 +408,7 @@ func AdminSiteDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r
}
}
//AdminSiteDeletePostHandler handles the removing of a site
// AdminSiteDeletePostHandler handles the removing of a site
func AdminSiteDeletePostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
reqVar := getVar(r, "siteID")

View File

@ -1,4 +1,4 @@
package controllers_test
package handler_test
import (
"fmt"
@ -7,7 +7,7 @@ import (
"strconv"
"testing"
"git.hoogi.eu/snafu/go-blog/controllers"
"git.hoogi.eu/snafu/go-blog/handler"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -92,7 +92,7 @@ func doGetSiteRequest(user reqUser, link string) (*models.Site, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.GetSiteHandler(ctx, rw, r.buildRequest())
tpl := handler.GetSiteHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -115,7 +115,7 @@ func doAdminGetSiteRequest(user reqUser, siteID int) (*models.Site, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminGetSiteHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminGetSiteHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -132,7 +132,7 @@ func doAdminListSitesRequest(user reqUser) ([]models.Site, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminSitesHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminSitesHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -156,7 +156,7 @@ func doAdminSiteCreateRequest(user reqUser, s *models.Site) (int, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminSiteNewPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminSiteNewPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return 0, tpl.Err
@ -185,7 +185,7 @@ func doAdminSitePublishRequest(user reqUser, siteID int) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminSitePublishPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminSitePublishPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err
@ -215,7 +215,7 @@ func doAdminSiteEditRequest(user reqUser, s *models.Site) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminSiteEditPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminSiteEditPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err
@ -238,7 +238,7 @@ func doAdminSiteDeleteRequest(user reqUser, siteID int) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminSiteDeletePostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminSiteDeletePostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err
@ -261,7 +261,7 @@ func doAdminSiteOrderRequest(user reqUser, siteID int) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminSiteOrderHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminSiteOrderHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -2,18 +2,18 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package controllers
package handler
import (
"fmt"
"net/http"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/middleware"
"git.hoogi.eu/snafu/go-blog/models"
)
//AdminUsersHandler returns an overview of the created users (admin only action)
// AdminUsersHandler returns an overview of the created users
func AdminUsersHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
page := getPageParam(r)
@ -73,7 +73,7 @@ func AdminUsersHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *htt
}
}
//AdminUserNewHandler returns the form for adding new user (admin only action)
// AdminUserNewHandler returns the form for adding new user
func AdminUserNewHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
return &middleware.Template{
Name: tplAdminUserNew,
@ -81,7 +81,7 @@ func AdminUserNewHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *h
}
}
//AdminUserNewPostHandler handles the creation of new users (admin only action)
// AdminUserNewPostHandler handles the creation of new users
func AdminUserNewPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
u := &models.User{
DisplayName: r.FormValue("displayname"),
@ -114,7 +114,7 @@ func AdminUserNewPostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
//AdminUserEditHandler returns the form for editing an user (admin only action)
// AdminUserEditHandler returns the form for editing an user
func AdminUserEditHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
userID, err := parseInt(getVar(r, "userID"))
@ -143,7 +143,7 @@ func AdminUserEditHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *
}
}
//AdminUserEditPostHandler handles the updating of an user (admin only action)
// AdminUserEditPostHandler handles the updating of an user
func AdminUserEditPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
userID, err := parseInt(getVar(r, "userID"))
@ -196,11 +196,11 @@ func AdminUserEditPostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
sids := ctx.SessionService.SessionProvider.SessionIDsFromValues("userid", u.ID)
sessions := ctx.SessionService.SessionProvider.FindByValue("userid", u.ID)
for _, id := range sids {
if session.SessionID() != id {
ctx.SessionService.SessionProvider.Remove(id)
for _, s := range sessions {
if session.SessionID() != s.SessionID() {
ctx.SessionService.SessionProvider.Remove(s.SessionID())
}
}
}
@ -212,7 +212,7 @@ func AdminUserEditPostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
}
}
//AdminUserDeleteHandler returns the form for removing user (admin only action)
// AdminUserDeleteHandler returns the form for removing a user
func AdminUserDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
userID, err := parseInt(getVar(r, "userID"))
@ -256,7 +256,7 @@ func AdminUserDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r
}
}
//AdminUserDeletePostHandler handles removing of a user (admin only action)
// AdminUserDeletePostHandler handles removing of a user
func AdminUserDeletePostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
userID, err := parseInt(getVar(r, "userID"))

View File

@ -1,4 +1,4 @@
package controllers
package handler
import (
"fmt"
@ -8,6 +8,7 @@ import (
"git.hoogi.eu/snafu/go-blog/models"
)
// AdminUserInviteNewHandler shows the form to invite an user
func AdminUserInviteNewHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
return &middleware.Template{
Name: tplAdminUserInviteNew,
@ -15,6 +16,7 @@ func AdminUserInviteNewHandler(ctx *middleware.AppContext, w http.ResponseWriter
}
}
// AdminUserInviteNewPostHandler handles the invitation, sends an activation mail to the invited user
func AdminUserInviteNewPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
user, _ := middleware.User(r)
@ -52,6 +54,7 @@ func AdminUserInviteNewPostHandler(ctx *middleware.AppContext, w http.ResponseWr
}
}
// AdminUserInviteResendPostHandler resends the activation link to the user
func AdminUserInviteResendPostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
inviteID, err := parseInt(getVar(r, "inviteID"))
@ -86,6 +89,7 @@ func AdminUserInviteResendPostHandler(ctx *middleware.AppContext, w http.Respons
}
}
// AdminUserInviteDeleteHandler shows the form to remove an user invitation
func AdminUserInviteDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
inviteID, err := parseInt(getVar(r, "inviteID"))
@ -124,6 +128,7 @@ func AdminUserInviteDeleteHandler(ctx *middleware.AppContext, w http.ResponseWri
}
}
// AdminUserInviteDeletePostHandler handles the removing of an user invitation
func AdminUserInviteDeletePostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
inviteID, err := parseInt(getVar(r, "inviteID"))

View File

@ -1,4 +1,4 @@
package controllers_test
package handler_test
import (
"net/http/httptest"
@ -6,7 +6,7 @@ import (
"strconv"
"testing"
"git.hoogi.eu/snafu/go-blog/controllers"
"git.hoogi.eu/snafu/go-blog/handler"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -70,7 +70,7 @@ func doAdminCreateUserInviteRequest(user reqUser, ui *models.UserInvite) (int, s
}
rw := httptest.NewRecorder()
tpl := controllers.AdminUserInviteNewPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminUserInviteNewPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return -1, "", tpl.Err
@ -93,7 +93,7 @@ func doAdminResendUserInviteRequest(user reqUser, inviteID int) (int, string, er
}
rw := httptest.NewRecorder()
tpl := controllers.AdminUserInviteResendPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminUserInviteResendPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return -1, "", tpl.Err
@ -116,7 +116,7 @@ func doAdminRemoveUserInviteRequest(user reqUser, inviteID int) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminUserInviteDeletePostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminUserInviteDeletePostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package controllers_test
package handler_test
import (
"fmt"
@ -11,7 +11,7 @@ import (
"strconv"
"testing"
"git.hoogi.eu/snafu/go-blog/controllers"
"git.hoogi.eu/snafu/go-blog/handler"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -110,7 +110,7 @@ func doAdminGetUserRequest(user reqUser, userID int) (*models.User, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminUserEditHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminUserEditHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -156,7 +156,7 @@ func doAdminEditUsersRequest(user reqUser, u *models.User) error {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminUserEditPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminUserEditPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return tpl.Err
@ -176,7 +176,7 @@ func doAdminListUsersRequest(user reqUser) ([]models.User, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminUsersHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminUsersHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return nil, tpl.Err
@ -202,7 +202,7 @@ func doAdminCreateUserRequest(user reqUser, u *models.User) (int, error) {
}
rw := httptest.NewRecorder()
tpl := controllers.AdminUserNewPostHandler(ctx, rw, r.buildRequest())
tpl := handler.AdminUserNewPostHandler(ctx, rw, r.buildRequest())
if tpl.Err != nil {
return 0, tpl.Err

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package controllers_test
package handler_test
import (
"bytes"
@ -20,13 +20,13 @@ import (
"path/filepath"
"testing"
"git.hoogi.eu/snafu/go-blog/components/database"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/components/mail"
"git.hoogi.eu/snafu/go-blog/crypt"
"git.hoogi.eu/snafu/go-blog/database"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/mail"
"git.hoogi.eu/snafu/go-blog/middleware"
"git.hoogi.eu/snafu/go-blog/models"
"git.hoogi.eu/snafu/go-blog/settings"
"git.hoogi.eu/snafu/go-blog/utils"
"git.hoogi.eu/snafu/session"
_ "github.com/mattn/go-sqlite3"
@ -64,58 +64,58 @@ func setup(t *testing.T) {
cfg.File.Location = os.TempDir()
userService := models.UserService{
Datasource: models.SQLiteUserDatasource{
userService := &models.UserService{
Datasource: &models.SQLiteUserDatasource{
SQLConn: db,
},
Config: cfg.User,
}
userInviteService := models.UserInviteService{
Datasource: models.SQLiteUserInviteDatasource{
userInviteService := &models.UserInviteService{
Datasource: &models.SQLiteUserInviteDatasource{
SQLConn: db,
},
UserService: userService,
}
articleService := models.ArticleService{
articleService := &models.ArticleService{
AppConfig: cfg.Application,
Datasource: models.SQLiteArticleDatasource{
Datasource: &models.SQLiteArticleDatasource{
SQLConn: db,
},
}
siteService := models.SiteService{
Datasource: models.SQLiteSiteDatasource{
siteService := &models.SiteService{
Datasource: &models.SQLiteSiteDatasource{
SQLConn: db,
},
}
fileService := models.FileService{
fileService := &models.FileService{
Config: cfg.File,
Datasource: models.SQLiteFileDatasource{
Datasource: &models.SQLiteFileDatasource{
SQLConn: db,
},
}
categoryService := models.CategoryService{
Datasource: models.SQLiteCategoryDatasource{
categoryService := &models.CategoryService{
Datasource: &models.SQLiteCategoryDatasource{
SQLConn: db,
},
}
tokenService := models.TokenService{
Datasource: models.SQLiteTokenDatasource{
tokenService := &models.TokenService{
Datasource: &models.SQLiteTokenDatasource{
SQLConn: db,
},
}
mailer := models.Mailer{
mailer := &models.Mailer{
Sender: MockSMTP{},
AppConfig: &cfg.Application,
}
sessionService := session.SessionService{
sessionService := session.Service{
Path: "/admin",
Name: "test-session",
HTTPOnly: true,
@ -145,9 +145,9 @@ func teardown() {
}
func fillSeeds(db *sql.DB) error {
salt := utils.GenerateSalt()
saltedPassword := utils.AppendBytes([]byte("123456789012"), salt)
password, err := utils.CryptPassword([]byte(saltedPassword), 12)
salt := crypt.GenerateSalt()
saltedPassword := append([]byte("123456789012"), salt[:]...)
password, err := crypt.CryptPassword([]byte(saltedPassword))
if err != nil {
return err

View File

@ -9,23 +9,23 @@ import (
"net/http"
)
//Error enriches the original go error type with
//DisplayMsg the description for the displaying message
//HTTPStatus the HTTP status code
//Err the internal error it should not be shown to the user
// Error enriches the original go error type with
// DisplayMsg the human readable display message
// HTTPStatus the HTTP status code
// Err the internal error
type Error struct {
DisplayMsg string `json:"display_message"`
HTTPStatus int `json:"status"`
Err error `json:"-"`
}
//New returns a new error
// New returns a new error
func New(httpStatus int, displayMsg string, err error) *Error {
return &Error{DisplayMsg: displayMsg, Err: err, HTTPStatus: httpStatus}
}
//PermissionDenied returns a permission denied message with code 403.
//The following display message is returned: "You are not allowed to [action] the [subject]."
// PermissionDenied returns a permission denied message with code 403.
// The following display message is returned: "You are not allowed to [action] the [subject]."
func PermissionDenied(action, subject string, err error) *Error {
return &Error{
HTTPStatus: http.StatusForbidden,
@ -34,8 +34,8 @@ func PermissionDenied(action, subject string, err error) *Error {
}
}
//NotFound returns a not found message with code 404.
//The following display message is returned: "The [res] was not found."
// NotFound returns a not found message with code 404.
// The following display message is returned: "The [res] was not found."
func NotFound(res string, err error) *Error {
return &Error{
HTTPStatus: http.StatusNotFound,
@ -44,8 +44,8 @@ func NotFound(res string, err error) *Error {
}
}
//ValueTooLong returns the following display message with code 422.
//Display message: "The value of [param] is too long. Maximum [nchars] characters are allowed."
// ValueTooLong returns the following display message with code 422.
// Display message: "The value of [param] is too long. Maximum [nchars] characters are allowed."
func ValueTooLong(param string, nchars int) *Error {
return &Error{
HTTPStatus: http.StatusUnprocessableEntity,
@ -54,18 +54,18 @@ func ValueTooLong(param string, nchars int) *Error {
}
}
//InternalServerError returns a internal server error message with code 500.
//Display message: "An internal server error occured."
// InternalServerError returns a internal server error message with code 500.
// Display message: "An internal server error occurred."
func InternalServerError(err error) *Error {
return &Error{
HTTPStatus: http.StatusInternalServerError,
DisplayMsg: "An internal server error occured.",
DisplayMsg: "An internal server error occurred.",
Err: err,
}
}
//ParameterMissing returns a parameter missing message with code 422.
//Display message: "The parameter [param] is invalid."
// ParameterMissing returns a parameter missing message with code 422.
// Display message: "The parameter [param] is invalid."
func ParameterMissing(param string, err error) *Error {
return &Error{
HTTPStatus: http.StatusUnprocessableEntity,
@ -74,8 +74,8 @@ func ParameterMissing(param string, err error) *Error {
}
}
//ValueRequired returns a value required message with code 422.
//Display message: "Please fill out the field [param]."
// ValueRequired returns a value required message with code 422.
// Display message: "Please fill out the field [param]."
func ValueRequired(param string) *Error {
return &Error{
HTTPStatus: http.StatusUnprocessableEntity,
@ -84,23 +84,10 @@ func ValueRequired(param string) *Error {
}
}
func Equals(a error, b error) bool {
v, ok := a.(*Error)
v2, ok2 := b.(*Error)
if ok && ok2 {
return v.Err == v2.Err
} else if !ok && !ok2 {
return v == v2
} else if ok && !ok2 {
return v.Err == b
} else if !ok && ok2 {
return a == v2.Err
}
return false
}
func (e Error) Error() string {
return fmt.Sprintf("code=[%d], error=[%s], displayMsg=[%s]", e.HTTPStatus, e.Err.Error(), e.DisplayMsg)
}
func (e *Error) Unwrap() error {
return e.Err
}

View File

@ -11,17 +11,12 @@ import (
"github.com/sirupsen/logrus"
)
//Log returns a new logrus instance
// Log returns a new logrus instance
var Log = logrus.New()
//InitLogger initializes the logger. Writes to file with the specified log level
//Valid log levels are:
// debug
// info (fallback)
// warn
// error
// fatal
// panic
// InitLogger initializes the logger
// Valid log levels are: debug|info|warn|error|fatal|panic
// Fallback: info
func InitLogger(w io.Writer, level string) {
level = strings.ToLower(level)

View File

@ -6,11 +6,11 @@ import (
"fmt"
"net/smtp"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/logger"
)
//Service holds configuration for the SMTP server
//The sender address and an optional subject prefix
// Service holds configuration for the SMTP server
// The sender address and an optional subject prefix
type Service struct {
SubjectPrefix string
SMTPConfig SMTPConfig
@ -25,8 +25,8 @@ func (m Mail) validate() error {
return nil
}
//NewMailService returns a new Service with specified config
func NewMailService(subjectPrefix, from string, smtpConfig SMTPConfig) Service {
// NewMailService returns a new Service with specified config
func NewMailService(subjectPrefix, from string, smtpConfig SMTPConfig) *Service {
s := Service{
SubjectPrefix: subjectPrefix,
From: from,
@ -35,8 +35,7 @@ func NewMailService(subjectPrefix, from string, smtpConfig SMTPConfig) Service {
go s.readBuffer()
return s
return &s
}
type Sender interface {
@ -44,16 +43,16 @@ type Sender interface {
SendAsync(m Mail)
}
//SMTPConfig holds the configuration for the SMTP server
// SMTPConfig holds the configuration for the SMTP server
type SMTPConfig struct {
Address string
Port int
User string
Helo string
HELO string
Password []byte
}
//Mail represents a mail
// Mail represents a mail
type Mail struct {
To string
Subject string
@ -89,7 +88,7 @@ func (s Service) SendAsync(m Mail) {
}()
}
//Send sends a mail over the configured SMTP server
// Send sends a mail over the configured SMTP server
func (s Service) Send(m Mail) error {
if len(s.SMTPConfig.User) > 0 && len(s.SMTPConfig.Password) > 0 {
auth := smtp.PlainAuth("", s.SMTPConfig.User, string(s.SMTPConfig.Password), s.SMTPConfig.Address)
@ -109,8 +108,8 @@ func (s Service) Send(m Mail) error {
return err
}
if len(s.SMTPConfig.Helo) > 0 {
if err := c.Hello(s.SMTPConfig.Helo); err != nil {
if len(s.SMTPConfig.HELO) > 0 {
if err := c.Hello(s.SMTPConfig.HELO); err != nil {
return err
}
}
@ -144,7 +143,7 @@ func (s Service) Send(m Mail) error {
}
}
var buffer = make(chan Mail, 10)
var buffer = make(chan Mail, 5)
func (s Service) readBuffer() {
for {

81
main.go
View File

@ -13,9 +13,9 @@ import (
"time"
"git.hoogi.eu/snafu/cfg"
"git.hoogi.eu/snafu/go-blog/components/database"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/components/mail"
"git.hoogi.eu/snafu/go-blog/database"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/mail"
m "git.hoogi.eu/snafu/go-blog/middleware"
"git.hoogi.eu/snafu/go-blog/models"
"git.hoogi.eu/snafu/go-blog/routers"
@ -36,19 +36,19 @@ func main() {
}()
configFiles := []cfg.File{
cfg.File{
{
Name: "go-blog.conf",
Path: ".",
Required: true,
},
cfg.File{
{
Name: "go-blog.conf",
Path: "./custom",
Required: false,
},
}
cfg, err := settings.MergeConfigs(configFiles)
config, err := settings.MergeConfigs(configFiles)
if err != nil {
exitCode = 1
@ -56,29 +56,29 @@ func main() {
return
}
cfg.BuildVersion = BuildVersion
cfg.BuildGitHash = GitHash
config.BuildVersion = BuildVersion
config.BuildGitHash = GitHash
if err = cfg.CheckConfig(); err != nil {
if err = config.CheckConfig(); err != nil {
exitCode = 1
fmt.Println(err)
return
}
if cfg.Environment == "prod" {
logFile, err := os.OpenFile(cfg.Log.File, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if config.Environment == "prod" {
logFile, err := os.OpenFile(config.Log.File, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {
fmt.Println(err)
exitCode = 1
}
logger.InitLogger(logFile, cfg.Log.Level)
logger.InitLogger(logFile, config.Log.Level)
} else {
logger.InitLogger(os.Stdout, cfg.Log.Level)
logger.InitLogger(os.Stdout, config.Log.Level)
}
csrf, err := cfg.GenerateCSRF()
csrf, err := config.GenerateCSRF()
if err != nil {
exitCode = 1
@ -91,10 +91,10 @@ func main() {
}
logger.Log.Infof("Go-Blog version: %s, commit: %s", BuildVersion, GitHash)
logger.Log.Infof("running in %s mode", cfg.Environment)
logger.Log.Infof("running in %s mode", config.Environment)
dbConf := database.SQLiteConfig{
File: cfg.Database.File,
File: config.Database.File,
}
db, err := dbConf.Open()
@ -112,7 +112,7 @@ func main() {
}
}()
ctx, err := context(db, cfg)
ctx, err := context(db, config)
if err != nil {
logger.Log.Error(err)
@ -120,22 +120,21 @@ func main() {
return
}
r := routers.InitRoutes(ctx, cfg)
r := routers.InitRoutes(ctx, config)
s := &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Server.Address, cfg.Server.Port),
Addr: fmt.Sprintf("%s:%d", config.Server.Address, config.Server.Port),
Handler: r,
ReadTimeout: 15 * time.Second,
WriteTimeout: 20 * time.Second,
MaxHeaderBytes: 1 << 20,
}
logger.Log.Infof("server will start at %s on port %d", cfg.Server.Address, cfg.Server.Port)
if cfg.Server.UseTLS {
err = s.ListenAndServeTLS(cfg.Server.Cert, cfg.Server.Key)
if config.Server.UseTLS {
logger.Log.Infof("server will start at https://%s:%d", config.Server.Address, config.Server.Port)
err = s.ListenAndServeTLS(config.Server.Cert, config.Server.Key)
} else {
logger.Log.Infof("server will start at http://%s:%d", config.Server.Address, config.Server.Port)
err = s.ListenAndServe()
}
@ -149,49 +148,49 @@ func main() {
func context(db *sql.DB, cfg *settings.Settings) (*m.AppContext, error) {
ic := loadUserInterceptor(cfg.User.InterceptorPlugin)
userService := models.UserService{
Datasource: models.SQLiteUserDatasource{
userService := &models.UserService{
Datasource: &models.SQLiteUserDatasource{
SQLConn: db,
},
Config: cfg.User,
UserInterceptor: ic,
}
userInviteService := models.UserInviteService{
Datasource: models.SQLiteUserInviteDatasource{
userInviteService := &models.UserInviteService{
Datasource: &models.SQLiteUserInviteDatasource{
SQLConn: db,
},
UserService: userService,
}
articleService := models.ArticleService{
articleService := &models.ArticleService{
AppConfig: cfg.Application,
Datasource: models.SQLiteArticleDatasource{
Datasource: &models.SQLiteArticleDatasource{
SQLConn: db,
},
}
siteService := models.SiteService{
Datasource: models.SQLiteSiteDatasource{
siteService := &models.SiteService{
Datasource: &models.SQLiteSiteDatasource{
SQLConn: db,
},
}
fileService := models.FileService{
fileService := &models.FileService{
Config: cfg.File,
Datasource: models.SQLiteFileDatasource{
Datasource: &models.SQLiteFileDatasource{
SQLConn: db,
},
}
categoryService := models.CategoryService{
Datasource: models.SQLiteCategoryDatasource{
categoryService := &models.CategoryService{
Datasource: &models.SQLiteCategoryDatasource{
SQLConn: db,
},
}
tokenService := models.TokenService{
Datasource: models.SQLiteTokenDatasource{
tokenService := &models.TokenService{
Datasource: &models.SQLiteTokenDatasource{
SQLConn: db,
},
}
@ -205,8 +204,8 @@ func context(db *sql.DB, cfg *settings.Settings) (*m.AppContext, error) {
sender := mail.NewMailService(cfg.Mail.SubjectPrefix, cfg.Mail.SenderAddress, smtpConfig)
mailer := models.Mailer{
Sender: &sender,
mailer := &models.Mailer{
Sender: sender,
AppConfig: &cfg.Application,
}
@ -221,7 +220,7 @@ func context(db *sql.DB, cfg *settings.Settings) (*m.AppContext, error) {
return nil, err
}
sessionService := session.SessionService{
sessionService := session.Service{
Path: cfg.Session.CookiePath,
Name: cfg.Session.CookieName,
Secure: cfg.Session.CookieSecure,

View File

@ -12,17 +12,17 @@ import (
"git.hoogi.eu/snafu/session"
)
//AppContext contains the services, session store, templates, ...
// AppContext contains the services, session store, templates, ...
type AppContext struct {
SessionService *session.SessionService
ArticleService models.ArticleService
CategoryService models.CategoryService
UserService models.UserService
UserInviteService models.UserInviteService
SiteService models.SiteService
FileService models.FileService
TokenService models.TokenService
Mailer models.Mailer
SessionService *session.Service
ArticleService *models.ArticleService
CategoryService *models.CategoryService
UserService *models.UserService
UserInviteService *models.UserInviteService
SiteService *models.SiteService
FileService *models.FileService
TokenService *models.TokenService
Mailer *models.Mailer
ConfigService *settings.Settings
Templates *template.Template
}

View File

@ -8,8 +8,8 @@ import (
"encoding/json"
"net/http"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -19,11 +19,12 @@ type JSONHandler struct {
Handler JHandler
}
//JHandler enriches handler with the AppContext
// JHandler enriches handler with the AppContext
type JHandler func(*AppContext, http.ResponseWriter, *http.Request) (*models.JSONData, error)
func (fn JSONHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
statusCode := 200
logWithIP := logger.Log.WithField("ip", getIP(r))
code := http.StatusOK
rw.Header().Set("Content-Type", "application/json")
@ -32,34 +33,50 @@ func (fn JSONHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
if err != nil {
switch e := err.(type) {
case *httperror.Error:
statusCode = e.HTTPStatus
code = e.HTTPStatus
default:
statusCode = 500
logger.Log.Error(e)
code = http.StatusInternalServerError
}
logger.Log.Error(err)
logWithIP.Error(err)
mjson, err2 := json.Marshal(err)
if err2 != nil {
logger.Log.Error(err2)
http.Error(rw, err2.Error(), http.StatusInternalServerError)
j, err := json.Marshal(err)
if err != nil {
logWithIP.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
rw.WriteHeader(statusCode)
rw.Write(mjson)
rw.WriteHeader(code)
_, err = rw.Write(j)
if err != nil {
logWithIP.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
return
}
mjson, err2 := json.Marshal(data)
j, err := json.Marshal(data)
if err2 != nil {
http.Error(rw, err2.Error(), http.StatusInternalServerError)
rw.WriteHeader(500)
if err != nil {
logWithIP.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.WriteHeader(statusCode)
rw.Write(mjson)
rw.WriteHeader(code)
_, err = rw.Write(j)
if err != nil {
logWithIP.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
rw.WriteHeader(http.StatusInternalServerError)
return
}
}

View File

@ -5,25 +5,25 @@
package middleware
import (
"encoding/base64"
"net"
"net/http"
"strings"
"time"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/utils"
"git.hoogi.eu/snafu/go-blog/logger"
)
var locals = [...]net.IPNet{
net.IPNet{
{
IP: net.IPv4(10, 0, 0, 0),
Mask: net.CIDRMask(8, 32),
},
net.IPNet{
{
IP: net.IPv4(172, 16, 0, 0),
Mask: net.CIDRMask(12, 32),
},
net.IPNet{
{
IP: net.IPv4(192, 168, 0, 0),
Mask: net.CIDRMask(16, 32),
},
@ -72,7 +72,7 @@ func setCookie(rw http.ResponseWriter, name, path, data string) {
c := &http.Cookie{
Name: name,
Path: path,
Value: utils.EncodeBase64(data),
Value: base64.StdEncoding.EncodeToString([]byte(data)),
}
http.SetCookie(rw, c)
@ -80,6 +80,7 @@ func setCookie(rw http.ResponseWriter, name, path, data string) {
func getFlash(w http.ResponseWriter, r *http.Request, name string) (string, error) {
c, err := r.Cookie(name)
if err != nil {
switch err {
case http.ErrNoCookie:
@ -88,7 +89,9 @@ func getFlash(w http.ResponseWriter, r *http.Request, name string) (string, erro
return "", err
}
}
value, err := utils.DecodeBase64(c.Value)
value, err := base64.StdEncoding.DecodeString(c.Value)
if err != nil {
return "", err
}
@ -98,9 +101,10 @@ func getFlash(w http.ResponseWriter, r *http.Request, name string) (string, erro
Name: name,
MaxAge: -1,
Expires: time.Unix(1, 0),
Path: "/"}
Path: "/",
}
http.SetCookie(w, dc)
return value, nil
return string(value), nil
}

View File

@ -16,17 +16,20 @@ import (
"time"
"git.hoogi.eu/snafu/cfg"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/models"
"git.hoogi.eu/snafu/go-blog/settings"
"git.hoogi.eu/snafu/go-blog/utils"
)
// Template contains the information about the template to render.
// The DisplayMsg in models.Error will be the ErrorMsg in the flash bubble,
// the SuccessMsg is an optional variable which is also displayed as a green flash bubble,
// both are appended to the data map with keys 'ErrorMsg' or 'SuccessMsg' in the AppHandler
// Active contains the current active navigation.
// Data the data which is injected into the templates.
// SuccessMsg is an optional variable which is displayed as a green message.
// WarnMsg is an optional variable which is displayed as an orange message.
// RedirectPath contains the path where the request should be redirected.
// Err will be shown as red message in templates. If it's a httperror, the display message will be shown,
// otherwise generich 'An internal error occurred' is shown.
type Template struct {
Name string
Active string
@ -37,19 +40,21 @@ type Template struct {
Err error
}
//Templates defines in which directory should be looked for template
// Templates defines the directory where the templates are located, the FuncMap are additional functions, which can
// be used in the templates.
type Templates struct {
Directory string
FuncMap template.FuncMap
}
//NotFound returned if no route matches
// NotFound returned if no route matches
func NotFound(ctx *AppContext, rw http.ResponseWriter, r *http.Request) *Template {
//For deleting flash cookies
getFlash(rw, r, "ErrorMsg")
getFlash(rw, r, "SuccessMsg")
session, _ := ctx.SessionService.Get(rw, r)
if session != nil && strings.HasPrefix(r.URL.EscapedPath(), "/admin") {
return &Template{
Name: "admin/error",
@ -63,8 +68,8 @@ func NotFound(ctx *AppContext, rw http.ResponseWriter, r *http.Request) *Templat
}
}
//FuncMap some function that can be used in templates
func FuncMap(ss models.SiteService, settings *settings.Settings) template.FuncMap {
// FuncMap some function that can be used in templates
func FuncMap(ss *models.SiteService, settings *settings.Settings) template.FuncMap {
return template.FuncMap{
"GetMetadata": func(data map[string]interface{}) template.HTML {
var meta, desc string
@ -157,8 +162,7 @@ func FuncMap(ss models.SiteService, settings *settings.Settings) template.FuncMa
return t.Time.In(time.Local).Format("January 2, 2006 at 3:04 PM")
},
"HumanizeFilesize": func(size int64) string {
fs := cfg.FileSize(size)
return fs.HumanReadable()
return cfg.FileSize(size).HumanReadable()
},
"FormatDateTime": func(t time.Time) string {
return t.In(time.Local).Format("January 2, 2006 at 3:04 PM")
@ -185,8 +189,7 @@ func FuncMap(ss models.SiteService, settings *settings.Settings) template.FuncMa
return template.HTML(models.MarkdownToHTML([]byte(s)))
},
"NToBr": func(in string) template.HTML {
out := models.NewlineToBr(models.EscapeHTML(in))
return template.HTML(out)
return template.HTML(models.NewlineToBr(models.EscapeHTML(in)))
},
"EscapeHTML": func(in string) string {
return html.EscapeString(in)
@ -204,7 +207,7 @@ func FuncMap(ss models.SiteService, settings *settings.Settings) template.FuncMa
}
}
//Load walks threw directory and parses templates ending with html
// Load walks threw directory and parses templates ending with html
func (ts Templates) Load() (*template.Template, error) {
tpl := template.New("").Funcs(ts.FuncMap)
@ -229,10 +232,10 @@ func (ts Templates) Load() (*template.Template, error) {
return tpl, err
}
//RedirectURL builds a URL for redirecting
// RedirectURL builds a URL for redirecting
func (t Template) RedirectURL() string {
if t.RedirectPath[0] == byte('/') {
return t.RedirectPath
}
return utils.AppendString("/", t.RedirectPath)
return "/" + t.RedirectPath
}

View File

@ -12,8 +12,8 @@ import (
"github.com/gorilla/csrf"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -23,59 +23,57 @@ var (
UserContextKey = contextKey("user")
)
//TemplateHandler enriches handlers with a application context containing 'services'
// TemplateHandler enriches handlers with a application context containing 'services'
type TemplateHandler struct {
AppCtx *AppContext
Handler Handler
}
//Handler enriches handler with the AppContext
// Handler enriches handler with the AppContext
type Handler func(*AppContext, http.ResponseWriter, *http.Request) *Template
func (fn TemplateHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
var errorMsg, warnMsg, successMsg string
statusCode := 200
ip := getIP(r)
en := logger.Log.WithField("ip", ip)
t := fn.Handler(fn.AppCtx, rw, r)
if t.Data == nil {
t.Data = make(map[string]interface{})
}
user, err := User(r)
if err == nil {
t.Data["currentUser"] = user
}
var errorMsg, warnMsg, successMsg string
successMsg = t.SuccessMsg
warnMsg = t.WarnMsg
code := http.StatusOK
logWithIP := logger.Log.WithField("ip", getIP(r))
t.Data["CSRFToken"] = csrf.Token(r)
if t.Err != nil {
switch e := t.Err.(type) {
case *httperror.Error:
statusCode = e.HTTPStatus
en.Error(e)
code = e.HTTPStatus
logWithIP.Error(e)
errorMsg = e.DisplayMsg
default:
en.Error(e)
errorMsg = "Sorry, an internal server error occured"
logWithIP.Error(e)
errorMsg = "Sorry, an internal server error occurred"
}
t.Data["ErrorMsg"] = errorMsg
}
if user, err := User(r); err == nil {
t.Data["currentUser"] = user
}
if len(t.RedirectPath) == 0 {
t.Data["SuccessMsg"] = successMsg
t.Data["WarnsMsg"] = warnMsg
fl, err := getFlash(rw, r, "SuccessMsg")
if err != nil {
logger.Log.Error(err)
logWithIP.Error(err)
} else if len(fl) > 0 {
t.Data["SuccessMsg"] = fl
}
@ -83,7 +81,7 @@ func (fn TemplateHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
fl, err = getFlash(rw, r, "ErrorMsg")
if err != nil {
en.Error(err)
logWithIP.Error(err)
} else if len(fl) > 0 {
t.Data["ErrorMsg"] = fl
}
@ -91,7 +89,7 @@ func (fn TemplateHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
fl, err = getFlash(rw, r, "WarnMsg")
if err != nil {
en.Error(err)
logWithIP.Error(err)
} else if len(fl) > 0 {
t.Data["WarnMsg"] = fl
}
@ -99,14 +97,14 @@ func (fn TemplateHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
t.Data[csrf.TemplateTag] = csrf.TemplateField(r)
t.Data["active"] = t.Active
rw.WriteHeader(statusCode)
rw.WriteHeader(code)
if err := fn.AppCtx.Templates.ExecuteTemplate(rw, t.Name, t.Data); err != nil {
en.Error(err)
logWithIP.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
} else {
statusCode = http.StatusFound
code = http.StatusFound
if len(errorMsg) > 0 {
setCookie(rw, "ErrorMsg", "/", errorMsg)
} else if len(warnMsg) > 0 {
@ -114,88 +112,110 @@ func (fn TemplateHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
} else if len(successMsg) > 0 {
setCookie(rw, "SuccessMsg", "/", successMsg)
}
http.Redirect(rw, r, path.Clean(t.RedirectURL()), statusCode)
http.Redirect(rw, r, path.Clean(t.RedirectURL()), code)
}
}
//AuthHandler checks if the user is authenticated; if not next handler in chain is not called
// AuthHandler checks if the user is authenticated; if not next handler in chain is not called
func (ctx AppContext) AuthHandler(handler http.Handler) http.Handler {
fn := func(rw http.ResponseWriter, r *http.Request) {
logWithIP := logger.Log.WithField("ip", getIP(r))
session, err := ctx.SessionService.Get(rw, r)
if err != nil {
logger.Log.Error(err)
logWithIP.Error(err)
rw.WriteHeader(http.StatusUnauthorized)
ctx.Templates.ExecuteTemplate(rw, "admin/login", map[string]interface{}{
if err := ctx.Templates.ExecuteTemplate(rw, "admin/login", map[string]interface{}{
"ErrorMsg": "Please provide login credentials.",
"state": r.URL.EscapedPath(),
csrf.TemplateTag: csrf.TemplateField(r),
})
}); err != nil {
logWithIP.Errorf("error while executing the template %v", err)
return
}
return
}
userid, ok := session.GetValue("userid").(int)
if !ok {
logger.Log.Errorf("userid is not an integer %v", userid)
logWithIP.Error(err)
rw.WriteHeader(http.StatusUnauthorized)
ctx.Templates.ExecuteTemplate(rw, "admin/login", map[string]interface{}{
if err := ctx.Templates.ExecuteTemplate(rw, "admin/login", map[string]interface{}{
"ErrorMsg": "Please provide login credentials.",
"state": r.URL.EscapedPath(),
csrf.TemplateTag: csrf.TemplateField(r),
})
}); err != nil {
logWithIP.Error(err)
return
}
return
}
u, err := ctx.UserService.GetByID(userid)
if err != nil {
logger.Log.Error(err)
logWithIP.Error(err)
rw.WriteHeader(http.StatusUnauthorized)
ctx.Templates.ExecuteTemplate(rw, "admin/login", map[string]interface{}{
if err := ctx.Templates.ExecuteTemplate(rw, "admin/login", map[string]interface{}{
"ErrorMsg": "Please provide login credentials.",
"state": r.URL.EscapedPath(),
csrf.TemplateTag: csrf.TemplateField(r),
})
}); err != nil {
logger.Log.Errorf("error while executing the template %v", err)
return
}
return
}
ctx := context.WithValue(r.Context(), UserContextKey, u)
handler.ServeHTTP(rw, r.WithContext(ctx))
handler.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), UserContextKey, u)))
}
return http.HandlerFunc(fn)
}
//RequireAdmin ensures that the user is an admin; if not next handler in chain is not called
// RequireAdmin ensures that the user is an admin, if not next handler in chain is not called.
func (ctx AppContext) RequireAdmin(handler http.Handler) http.Handler {
fn := func(rw http.ResponseWriter, r *http.Request) {
logWithIP := logger.Log.WithField("ip", getIP(r))
u, err := User(r)
if err != nil {
logger.Log.Error(err)
ctx.Templates.ExecuteTemplate(rw, "admin/error", map[string]interface{}{
"ErrorMsg": "An internal server error occured",
})
logWithIP.Error(err)
if err := ctx.Templates.ExecuteTemplate(rw, "admin/error", map[string]interface{}{
"ErrorMsg": "An internal server error occurred",
}); err != nil {
logWithIP.Errorf("error while executing the template %v", err)
return
}
return
}
if u.IsAdmin == false {
ctx.Templates.ExecuteTemplate(rw, "admin/error", map[string]interface{}{
if err := ctx.Templates.ExecuteTemplate(rw, "admin/error", map[string]interface{}{
"ErrorMsg": "You have not the permissions to execute this action",
"currentUser": u,
})
}); err != nil {
logWithIP.Errorf("error while executing the template %v", err)
return
}
return
}
handler.ServeHTTP(rw, r)
}
return http.HandlerFunc(fn)
}
//User gets the user from the request context
// User gets the user from the request context
func User(r *http.Request) (*models.User, error) {
v := r.Context().Value(UserContextKey)
if v == nil {
return nil, httperror.InternalServerError(errors.New("user is not available in context. is the authentication handler in chain?"))
return nil, httperror.InternalServerError(errors.New("user is not available in context"))
}
return v.(*models.User), nil

View File

@ -5,7 +5,7 @@ import (
"encoding/xml"
"net/http"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/models"
)
@ -15,32 +15,40 @@ type XMLHandler struct {
Handler XHandler
}
//XNLHandler enriches handler with the AppContext
// XHandler enriches handler with the AppContext
type XHandler func(*AppContext, http.ResponseWriter, *http.Request) (*models.XMLData, error)
func (fn XMLHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
logWithIP := logger.Log.WithField("ip", getIP(r))
rw.Header().Set("Content-Type", "application/xml")
h, err := fn.Handler(fn.AppCtx, rw, r)
if err != nil {
logger.Log.Error(err)
logWithIP.Error(err)
x, err2 := xml.Marshal(err)
x, err := xml.Marshal(err)
if err2 != nil {
logger.Log.Error(err2)
http.Error(rw, err2.Error(), http.StatusInternalServerError)
if err != nil {
logWithIP.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
if _, err = rw.Write(x); err != nil {
logWithIP.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
rw.Write(x)
return
}
x, err2 := xml.MarshalIndent(h.Data, "", "\t")
if err2 != nil {
logWithIP.Error(err)
http.Error(rw, err2.Error(), http.StatusInternalServerError)
return
}
@ -55,5 +63,9 @@ func (fn XMLHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
x = bytes.Replace(x, []byte("&gt;"), []byte("&#x3e;"), -1) // >
}
rw.Write(x)
if _, err := rw.Write(x); err != nil {
logger.Log.Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
}

View File

@ -4,12 +4,12 @@
package models
// Action this type is used for YES/NO actions see template/admin/action.html
// Title is shown in the headline
// ActionURL defines where the form should be sent
// BackLinkURL defines where to go back (if clicking on cancel)
// WarnMsg defines an optional warning which is shown above the description
// Description describes what action the user has to decide
// Action this type is used for YES/NO actions see template/admin/action.html.
// Title is shown in the headline.
// ActionURL defines where the form should be sent.
// BackLinkURL defines where to go back (if clicking on cancel).
// WarnMsg defines an optional warning which is shown above the description.
// Description describes what question the user has to decide.
type Action struct {
ID string
Title string

View File

@ -14,9 +14,9 @@ import (
"strings"
"time"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/settings"
"git.hoogi.eu/snafu/go-blog/utils"
"git.hoogi.eu/snafu/go-blog/slug"
)
// Article represents an article
@ -37,7 +37,7 @@ type Article struct {
CName sql.NullString
}
//ArticleDatasourceService defines an interface for CRUD operations of articles
// ArticleDatasourceService defines an interface for CRUD operations of articles
type ArticleDatasourceService interface {
Create(a *Article) (int, error)
List(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]Article, error)
@ -53,24 +53,28 @@ const (
maxHeadlineSize = 150
)
//SlugEscape escapes the slug for use in URLs
// SlugEscape escapes the slug for use in URLs
func (a Article) SlugEscape() string {
spl := strings.Split(a.Slug, "/")
return fmt.Sprintf("%s/%s/%s", spl[0], spl[1], url.PathEscape(spl[2]))
}
func (a *Article) buildSlug(now time.Time, suffix int) string {
return utils.AppendString(strconv.Itoa(now.Year()), "/", strconv.Itoa(int(now.Month())), "/", utils.CreateURLSafeSlug(a.Headline, suffix))
var sb strings.Builder
sb.WriteString(strconv.Itoa(now.Year()))
sb.WriteString("/")
sb.WriteString(strconv.Itoa(int(now.Month())))
sb.WriteString("/")
sb.WriteString(slug.CreateURLSafeSlug(a.Headline, suffix))
return sb.String()
}
func (a *Article) slug(as ArticleService, now time.Time) error {
func (a *Article) slug(as *ArticleService, now time.Time) error {
for i := 0; i < 10; i++ {
a.Slug = a.buildSlug(now, i)
_, err := as.Datasource.GetBySlug(a.Slug, nil, All)
if err != nil {
if err == sql.ErrNoRows {
if _, err := as.Datasource.GetBySlug(a.Slug, nil, All); err != nil {
if errors.Is(err, sql.ErrNoRows) {
break
}
return err
@ -102,14 +106,14 @@ func (a *Article) validate() error {
return nil
}
//ArticleService containing the service to access articles
// ArticleService containing the service to access articles
type ArticleService struct {
Datasource ArticleDatasourceService
AppConfig settings.Application
}
// Create creates an article
func (as ArticleService) Create(a *Article) (int, error) {
func (as *ArticleService) Create(a *Article) (int, error) {
now := time.Now()
a.PublishedOn = NullTime{Time: now, Valid: true}
@ -118,22 +122,15 @@ func (as ArticleService) Create(a *Article) (int, error) {
return 0, err
}
err := a.slug(as, now)
if err != nil {
if err := a.slug(as, now); err != nil {
return -1, err
}
artID, err := as.Datasource.Create(a)
if err != nil {
return 0, err
}
return artID, nil
return as.Datasource.Create(a)
}
//Update updates an article
func (as ArticleService) Update(a *Article, u *User, updateSlug bool) error {
// Update updates an article
func (as *ArticleService) Update(a *Article, u *User, updateSlug bool) error {
if err := a.validate(); err != nil {
return err
}
@ -148,9 +145,8 @@ func (as ArticleService) Update(a *Article, u *User, updateSlug bool) error {
a.Slug = oldArt.Slug
} else {
now := time.Now()
err := a.slug(as, now)
if err != nil {
if err := a.slug(as, now); err != nil {
return err
}
}
@ -164,8 +160,8 @@ func (as ArticleService) Update(a *Article, u *User, updateSlug bool) error {
return as.Datasource.Update(a)
}
//Publish publishes or 'unpublishes' an article
func (as ArticleService) Publish(id int, u *User) error {
// Publish publishes or 'unpublishes' an article
func (as *ArticleService) Publish(id int, u *User) error {
a, err := as.Datasource.Get(id, nil, All)
if err != nil {
@ -181,8 +177,8 @@ func (as ArticleService) Publish(id int, u *User) error {
return as.Datasource.Publish(a)
}
//Delete deletes an article
func (as ArticleService) Delete(id int, u *User) error {
// Delete deletes an article
func (as *ArticleService) Delete(id int, u *User) error {
a, err := as.Datasource.Get(id, nil, All)
if err != nil {
@ -198,13 +194,13 @@ func (as ArticleService) Delete(id int, u *User) error {
return as.Datasource.Delete(a.ID)
}
// GetBySlug gets a article by the slug.
// GetBySlug gets an article by the slug.
// The publishedCriteria defines whether the published and/or unpublished articles should be considered
func (as ArticleService) GetBySlug(s string, u *User, pc PublishedCriteria) (*Article, error) {
func (as *ArticleService) GetBySlug(s string, u *User, pc PublishedCriteria) (*Article, error) {
a, err := as.Datasource.GetBySlug(s, u, pc)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return nil, httperror.NotFound("article", err)
}
return nil, err
@ -223,11 +219,11 @@ func (as ArticleService) GetBySlug(s string, u *User, pc PublishedCriteria) (*Ar
// GetByID get a article by the id.
// The publishedCriteria defines whether the published and/or unpublished articles should be considered
func (as ArticleService) GetByID(id int, u *User, pc PublishedCriteria) (*Article, error) {
func (as *ArticleService) GetByID(id int, u *User, pc PublishedCriteria) (*Article, error) {
a, err := as.Datasource.Get(id, u, pc)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return nil, httperror.NotFound("article", fmt.Errorf("the article with id %d was not found", id))
}
return nil, err
@ -246,18 +242,18 @@ func (as ArticleService) GetByID(id int, u *User, pc PublishedCriteria) (*Articl
// Count returns the number of articles.
// The publishedCriteria defines whether the published and/or unpublished articles should be considered
func (as ArticleService) Count(u *User, c *Category, pc PublishedCriteria) (int, error) {
func (as *ArticleService) Count(u *User, c *Category, pc PublishedCriteria) (int, error) {
return as.Datasource.Count(u, c, pc)
}
// List returns all article by the slug.
// The publishedCriteria defines whether the published and/or unpublished articles should be considered
func (as ArticleService) List(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]Article, error) {
func (as *ArticleService) List(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]Article, error) {
return as.Datasource.List(u, c, p, pc)
}
// RSSFeed receives a specified number of articles in RSS
func (as ArticleService) RSSFeed(p *Pagination, pc PublishedCriteria) (RSS, error) {
func (as *ArticleService) RSSFeed(p *Pagination, pc PublishedCriteria) (RSS, error) {
c := RSSChannel{
Title: as.AppConfig.Title,
Link: as.AppConfig.Domain,
@ -272,7 +268,8 @@ func (as ArticleService) RSSFeed(p *Pagination, pc PublishedCriteria) (RSS, erro
return RSS{}, err
}
items := []RSSItem{}
var items []RSSItem
for _, a := range articles {
link := fmt.Sprint(as.AppConfig.Domain, "/article/by-id/", a.ID)
item := RSSItem{
@ -300,7 +297,7 @@ type IndexArticle struct {
Articles []Article
}
func (as ArticleService) Index(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]IndexArticle, error) {
func (as *ArticleService) Index(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]IndexArticle, error) {
articles, err := as.Datasource.List(u, c, p, pc)
if err != nil {

View File

@ -1,8 +1,9 @@
package models
import (
"bytes"
"database/sql"
"git.hoogi.eu/snafu/go-blog/logger"
"strings"
"time"
)
@ -12,7 +13,7 @@ type SQLiteArticleDatasource struct {
}
// Create creates an article
func (rdb SQLiteArticleDatasource) Create(a *Article) (int, error) {
func (rdb *SQLiteArticleDatasource) Create(a *Article) (int, error) {
res, err := rdb.SQLConn.Exec("INSERT INTO article (headline, teaser, content, slug, published_on, published, last_modified, category_id, user_id) "+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
a.Headline,
@ -38,16 +39,20 @@ func (rdb SQLiteArticleDatasource) Create(a *Article) (int, error) {
return int(id), nil
}
//List returns a slice of articles; if the user is not nil the number of articles for this explcit user is returned
//the PublishedCritera specifies which articles should be considered
func (rdb SQLiteArticleDatasource) List(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]Article, error) {
// List returns a slice of articles; if the user is not nil the number of articles for this explcit user is returned
// the PublishedCritera specifies which articles should be considered
func (rdb *SQLiteArticleDatasource) List(u *User, c *Category, p *Pagination, pc PublishedCriteria) ([]Article, error) {
rows, err := selectArticlesStmt(rdb.SQLConn, u, c, p, pc)
if err != nil {
return nil, err
}
defer rows.Close()
defer func() {
if err := rows.Close(); err != nil {
logger.Log.Error(err)
}
}()
articles := []Article{}
@ -68,18 +73,17 @@ func (rdb SQLiteArticleDatasource) List(u *User, c *Category, p *Pagination, pc
if err := rows.Err(); err != nil {
return nil, err
}
return articles, nil
return articles, nil
}
//Count returns the number of article found; if the user is not nil the number of articles for this explcit user is returned
//the PublishedCritera specifies which articles should be considered
func (rdb SQLiteArticleDatasource) Count(u *User, c *Category, pc PublishedCriteria) (int, error) {
// Count returns the number of article found; if the user is not nil the number of articles for this explcit user is returned
// the PublishedCritera specifies which articles should be considered
func (rdb *SQLiteArticleDatasource) Count(u *User, c *Category, pc PublishedCriteria) (int, error) {
var total int
var stmt strings.Builder
var args []interface{}
var stmt bytes.Buffer
stmt.WriteString("SELECT count(a.id) FROM article a ")
if c != nil {
@ -117,9 +121,9 @@ func (rdb SQLiteArticleDatasource) Count(u *User, c *Category, pc PublishedCrite
return total, nil
}
//Get returns a article by its id; if the user is not nil the article for this explcit user is returned
//the PublishedCritera specifies which articles should be considered
func (rdb SQLiteArticleDatasource) Get(articleID int, u *User, pc PublishedCriteria) (*Article, error) {
// Get returns a article by its id; if the user is not nil the article for this explcit user is returned
// the PublishedCritera specifies which articles should be considered
func (rdb *SQLiteArticleDatasource) Get(articleID int, u *User, pc PublishedCriteria) (*Article, error) {
var a Article
var ru User
@ -133,9 +137,9 @@ func (rdb SQLiteArticleDatasource) Get(articleID int, u *User, pc PublishedCrite
return &a, nil
}
//GetBySlug returns a article by its slug; if the user is not nil the article for this explcit user is returned
//the PublishedCritera specifies which articles should be considered
func (rdb SQLiteArticleDatasource) GetBySlug(slug string, u *User, pc PublishedCriteria) (*Article, error) {
// GetBySlug returns a article by its slug; if the user is not nil the article for this explcit user is returned
// the PublishedCritera specifies which articles should be considered
func (rdb *SQLiteArticleDatasource) GetBySlug(slug string, u *User, pc PublishedCriteria) (*Article, error) {
var a Article
var ru User
@ -150,7 +154,7 @@ func (rdb SQLiteArticleDatasource) GetBySlug(slug string, u *User, pc PublishedC
}
// Update updates an aricle
func (rdb SQLiteArticleDatasource) Update(a *Article) error {
func (rdb *SQLiteArticleDatasource) Update(a *Article) error {
if _, err := rdb.SQLConn.Exec("UPDATE article SET headline=?, teaser=?, slug=?, content=?, last_modified=?, category_id=? WHERE id=? ", a.Headline, &a.Teaser, a.Slug,
a.Content, time.Now(), a.CID, a.ID); err != nil {
return err
@ -160,7 +164,7 @@ func (rdb SQLiteArticleDatasource) Update(a *Article) error {
}
// Publish checks if the article is published or not - switches the appropriate status
func (rdb SQLiteArticleDatasource) Publish(a *Article) error {
func (rdb *SQLiteArticleDatasource) Publish(a *Article) error {
publishOn := NullTime{Valid: false}
if !a.Published {
@ -176,7 +180,7 @@ func (rdb SQLiteArticleDatasource) Publish(a *Article) error {
}
// Delete deletes the article specified by the articleID
func (rdb SQLiteArticleDatasource) Delete(articleID int) error {
func (rdb *SQLiteArticleDatasource) Delete(articleID int) error {
if _, err := rdb.SQLConn.Exec("DELETE FROM article WHERE id=? ", articleID); err != nil {
return err
}
@ -184,7 +188,7 @@ func (rdb SQLiteArticleDatasource) Delete(articleID int) error {
}
func selectArticleStmt(db *sql.DB, articleID int, slug string, u *User, pc PublishedCriteria) *sql.Row {
var stmt bytes.Buffer
var stmt strings.Builder
var args []interface{}
@ -211,19 +215,21 @@ func selectArticleStmt(db *sql.DB, articleID int, slug string, u *User, pc Publi
stmt.WriteString("AND a.id=? ")
args = append(args, articleID)
}
if u != nil {
if !u.IsAdmin {
stmt.WriteString("AND a.user_id=? ")
args = append(args, u.ID)
}
}
stmt.WriteString("LIMIT 1")
return db.QueryRow(stmt.String(), args...)
}
func selectArticlesStmt(db *sql.DB, u *User, c *Category, p *Pagination, pc PublishedCriteria) (*sql.Rows, error) {
var stmt bytes.Buffer
var stmt strings.Builder
var args []interface{}
stmt.WriteString("SELECT a.id, a.headline, a.teaser, a.content, a.published, a.published_on, a.slug, a.last_modified, ")

View File

@ -8,8 +8,8 @@ import (
"strings"
"time"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/utils"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/slug"
)
type Category struct {
@ -39,6 +39,7 @@ func (c *Category) validate() error {
if c.Author == nil {
return httperror.InternalServerError(errors.New("category validation failed - the author is missing"))
}
return nil
}
@ -52,21 +53,21 @@ type CategoryDatasourceService interface {
Delete(categoryID int) error
}
//CategoryService containing the service to access categories
// CategoryService containing the service to access categories
type CategoryService struct {
Datasource CategoryDatasourceService
}
//SlugEscape escapes the slug for use in URLs
// SlugEscape escapes the slug for use in URLs
func (c Category) SlugEscape() string {
return url.PathEscape(c.Slug)
}
func (cs CategoryService) GetBySlug(s string, fc FilterCriteria) (*Category, error) {
func (cs *CategoryService) GetBySlug(s string, fc FilterCriteria) (*Category, error) {
c, err := cs.Datasource.GetBySlug(s, fc)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return nil, httperror.NotFound("category", fmt.Errorf("the category with slug %s was not found", s))
}
return nil, err
@ -75,35 +76,34 @@ func (cs CategoryService) GetBySlug(s string, fc FilterCriteria) (*Category, err
return c, nil
}
func (cs CategoryService) GetByID(id int, fc FilterCriteria) (*Category, error) {
func (cs *CategoryService) GetByID(id int, fc FilterCriteria) (*Category, error) {
c, err := cs.Datasource.Get(id, fc)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return nil, httperror.NotFound("category", fmt.Errorf("the category with id %d was not found", id))
}
return nil, err
}
return c, nil
}
func (cs CategoryService) Count(fc FilterCriteria) (int, error) {
func (cs *CategoryService) Count(fc FilterCriteria) (int, error) {
return cs.Datasource.Count(fc)
}
func (cs CategoryService) List(fc FilterCriteria) ([]Category, error) {
func (cs *CategoryService) List(fc FilterCriteria) ([]Category, error) {
return cs.Datasource.List(fc)
}
//Create creates a category
func (cs CategoryService) Create(c *Category) (int, error) {
// Create creates a category
func (cs *CategoryService) Create(c *Category) (int, error) {
for i := 0; i < 10; i++ {
c.Slug = utils.CreateURLSafeSlug(c.Name, i)
_, err := cs.Datasource.GetBySlug(c.Slug, AllCategories)
if err != nil {
if err == sql.ErrNoRows {
c.Slug = slug.CreateURLSafeSlug(c.Name, i)
if _, err := cs.Datasource.GetBySlug(c.Slug, AllCategories); err != nil {
if errors.Is(err, sql.ErrNoRows) {
break
}
return -1, err
@ -114,16 +114,11 @@ func (cs CategoryService) Create(c *Category) (int, error) {
return 0, err
}
cid, err := cs.Datasource.Create(c)
if err != nil {
return 0, err
}
return cid, nil
return cs.Datasource.Create(c)
}
//Update updates a category
func (cs CategoryService) Update(c *Category) error {
// Update updates a category
func (cs *CategoryService) Update(c *Category) error {
if err := c.validate(); err != nil {
return err
}
@ -131,8 +126,8 @@ func (cs CategoryService) Update(c *Category) error {
return cs.Datasource.Update(c)
}
//Delete removes a category
func (cs CategoryService) Delete(id int) error {
// Delete removes a category
func (cs *CategoryService) Delete(id int) error {
c, err := cs.Datasource.Get(id, AllCategories)
if err != nil {

View File

@ -3,15 +3,17 @@ package models
import (
"bytes"
"database/sql"
"git.hoogi.eu/snafu/go-blog/logger"
"strings"
"time"
)
// SQLiteArticleDatasource providing an implementation of ArticleDatasourceService for SQLite
// SQLiteCategoryDatasource providing an implementation of CategoryDatasourceService for SQLite
type SQLiteCategoryDatasource struct {
SQLConn *sql.DB
}
func (rdb SQLiteCategoryDatasource) Create(c *Category) (int, error) {
func (rdb *SQLiteCategoryDatasource) Create(c *Category) (int, error) {
res, err := rdb.SQLConn.Exec("INSERT INTO category (name, slug, last_modified, user_id) "+
"VALUES (?, ?, ?, ?)",
c.Name,
@ -32,10 +34,9 @@ func (rdb SQLiteCategoryDatasource) Create(c *Category) (int, error) {
return int(id), nil
}
func (rdb SQLiteCategoryDatasource) List(fc FilterCriteria) ([]Category, error) {
var stmt bytes.Buffer
func (rdb *SQLiteCategoryDatasource) List(fc FilterCriteria) ([]Category, error) {
var args []interface{}
var stmt strings.Builder
stmt.WriteString("SELECT DISTINCT c.id, c.name, c.slug, c.last_modified, ")
stmt.WriteString("u.id, u.display_name, u.username, u.email, u.is_admin ")
@ -61,9 +62,13 @@ func (rdb SQLiteCategoryDatasource) List(fc FilterCriteria) ([]Category, error)
return nil, err
}
defer rows.Close()
defer func() {
if err := rows.Close(); err != nil {
logger.Log.Error(err)
}
}()
cs := []Category{}
var cs []Category
for rows.Next() {
var c Category
@ -85,7 +90,7 @@ func (rdb SQLiteCategoryDatasource) List(fc FilterCriteria) ([]Category, error)
return cs, nil
}
func (rdb SQLiteCategoryDatasource) Count(fc FilterCriteria) (int, error) {
func (rdb *SQLiteCategoryDatasource) Count(fc FilterCriteria) (int, error) {
var total int
if err := rdb.SQLConn.QueryRow("SELECT count(id) FROM category ").Scan(&total); err != nil {
@ -95,7 +100,7 @@ func (rdb SQLiteCategoryDatasource) Count(fc FilterCriteria) (int, error) {
return total, nil
}
func (rdb SQLiteCategoryDatasource) Get(categoryID int, fc FilterCriteria) (*Category, error) {
func (rdb *SQLiteCategoryDatasource) Get(categoryID int, fc FilterCriteria) (*Category, error) {
var stmt bytes.Buffer
stmt.WriteString("SELECT c.id, c.name, c.slug, c.last_modified, ")
@ -131,14 +136,15 @@ func (rdb SQLiteCategoryDatasource) Get(categoryID int, fc FilterCriteria) (*Cat
return &c, nil
}
func (rdb SQLiteCategoryDatasource) GetBySlug(slug string, fc FilterCriteria) (*Category, error) {
var stmt bytes.Buffer
func (rdb *SQLiteCategoryDatasource) GetBySlug(slug string, fc FilterCriteria) (*Category, error) {
var stmt strings.Builder
stmt.WriteString("SELECT c.id, c.name, c.slug, c.last_modified, ")
stmt.WriteString("u.id, u.display_name, u.username, u.email, u.is_admin ")
stmt.WriteString("FROM category as c ")
stmt.WriteString("INNER JOIN user as u ")
stmt.WriteString("ON u.id = c.user_id ")
if fc == CategoriesWithPublishedArticles {
stmt.WriteString("INNER JOIN article as a ")
stmt.WriteString("ON c.id = a.category_id ")
@ -166,24 +172,19 @@ func (rdb SQLiteCategoryDatasource) GetBySlug(slug string, fc FilterCriteria) (*
return &c, nil
}
func (rdb SQLiteCategoryDatasource) Update(c *Category) error {
_, err := rdb.SQLConn.Exec("UPDATE category SET name=?, slug=?, last_modified=?, user_id=? WHERE id=?",
c.Name,
c.Slug,
time.Now(),
c.Author.ID,
c.ID)
if err != nil {
func (rdb *SQLiteCategoryDatasource) Update(c *Category) error {
if _, err := rdb.SQLConn.Exec("UPDATE category SET name=?, slug=?, last_modified=?, user_id=? WHERE id=?",
c.Name, c.Slug, time.Now(), c.Author.ID, c.ID); err != nil {
return err
}
return nil
}
func (rdb SQLiteCategoryDatasource) Delete(categoryID int) error {
func (rdb *SQLiteCategoryDatasource) Delete(categoryID int) error {
if _, err := rdb.SQLConn.Exec("DELETE FROM category WHERE id=?", categoryID); err != nil {
return err
}
return nil
}

View File

@ -4,12 +4,12 @@
package models
// JSONData represents arbritary JSON data
// JSONData represents arbitrary JSON data
type JSONData struct {
Data interface{} `json:"data,-" xml:"data,-"`
}
// XMLData represents arbritary XML data
// XMLData represents arbitrary XML data
type XMLData struct {
Data interface{} `xml:"data,-"`
HexEncode bool `xml:"-"`

View File

@ -11,14 +11,14 @@ import (
"path/filepath"
"strings"
"time"
"unicode"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/settings"
"git.hoogi.eu/snafu/go-blog/utils"
)
//File represents a file
// File represents a file
type File struct {
ID int
UniqueName string `json:"unique_name"`
@ -33,15 +33,15 @@ type File struct {
Author *User
}
//FileInfo contains Path, Name and Extension of a file.
//Use SplitFilename to split the information from a filename
// FileInfo contains Path, Name and Extension of a file.
// Use SplitFilename to split the information from a filename
type FileInfo struct {
Path string
Name string
Extension string
}
//FileDatasourceService defines an interface for CRUD operations of files
// FileDatasourceService defines an interface for CRUD operations of files
type FileDatasourceService interface {
Create(f *File) (int, error)
Get(fileID int, u *User) (*File, error)
@ -67,12 +67,16 @@ func (f *File) validate() error {
func (f File) randomFilename() string {
var buf bytes.Buffer
sanFilename := utils.SanitizeFilename(f.FileInfo.Name)
sanFilename := sanitizeFilename(f.FileInfo.Name)
if len(sanFilename) == 0 {
sanFilename = "unnamed"
}
buf.WriteString(sanFilename)
buf.WriteString(f.FileInfo.Extension)
return buf.String()
}
@ -100,37 +104,37 @@ func SplitFilename(filename string) FileInfo {
}
}
//FileService containing the service to interact with files
// FileService containing the service to interact with files
type FileService struct {
Datasource FileDatasourceService
Config settings.File
}
//GetByID returns the file based on the fileID; it the user is given and it is a non admin
//only file specific to this user is returned
func (fs FileService) GetByID(fileID int, u *User) (*File, error) {
// GetByID returns the file based on the fileID; it the user is given and it is a non admin
// only file specific to this user is returned
func (fs *FileService) GetByID(fileID int, u *User) (*File, error) {
return fs.Datasource.Get(fileID, u)
}
//GetByUniqueName returns the file based on the unique name; it the user is given and it is a non admin
//only file specific to this user is returned
func (fs FileService) GetByUniqueName(uniqueName string, u *User) (*File, error) {
// GetByUniqueName returns the file based on the unique name; it the user is given and it is a non admin
// only file specific to this user is returned
func (fs *FileService) GetByUniqueName(uniqueName string, u *User) (*File, error) {
return fs.Datasource.GetByUniqueName(uniqueName, u)
}
//List returns a list of files based on the filename; it the user is given and it is a non admin
//only files specific to this user are returned
func (fs FileService) List(u *User, p *Pagination) ([]File, error) {
// List returns a list of files based on the filename; it the user is given and it is a non admin
// only files specific to this user are returned
func (fs *FileService) List(u *User, p *Pagination) ([]File, error) {
return fs.Datasource.List(u, p)
}
//Count returns a number of files based on the filename; it the user is given and it is a non admin
//only files specific to this user are counted
func (fs FileService) Count(u *User) (int, error) {
// Count returns a number of files based on the filename; it the user is given and it is a non admin
// only files specific to this user are counted
func (fs *FileService) Count(u *User) (int, error) {
return fs.Datasource.Count(u)
}
func (fs FileService) ToggleInline(fileID int, u *User) error {
func (fs *FileService) ToggleInline(fileID int, u *User) error {
f, err := fs.Datasource.Get(fileID, u)
if err != nil {
@ -152,8 +156,8 @@ func (fs FileService) ToggleInline(fileID int, u *User) error {
return fs.Datasource.Update(f)
}
//Delete deletes a file based on fileID; users which are not the owner are not allowed to remove files; except admins
func (fs FileService) Delete(fileID int, u *User) error {
// Delete deletes a file based on fileID; users which are not the owner are not allowed to remove files; except admins
func (fs *FileService) Delete(fileID int, u *User) error {
file, err := fs.Datasource.Get(fileID, u)
if err != nil {
@ -175,8 +179,8 @@ func (fs FileService) Delete(fileID int, u *User) error {
return os.Remove(filepath.Join(fs.Config.Location, file.UniqueName))
}
//Upload uploaded files will be saved at the configured file location, filename is saved in the database
func (fs FileService) Upload(f *File) (int, error) {
// Upload uploaded files will be saved at the configured file location, filename is saved in the database
func (fs *FileService) Upload(f *File) (int, error) {
if err := f.validate(); err != nil {
return -1, err
}
@ -218,9 +222,7 @@ func (fs FileService) Upload(f *File) (int, error) {
fi := filepath.Join(fs.Config.Location, f.UniqueName)
err = ioutil.WriteFile(fi, f.Data, 0640)
if err != nil {
if err = ioutil.WriteFile(fi, f.Data, 0640); err != nil {
return -1, err
}
@ -238,3 +240,36 @@ func (fs FileService) Upload(f *File) (int, error) {
return i, nil
}
var filenameSubs = map[rune]string{
'/': "",
'\\': "",
':': "",
'*': "",
'?': "",
'"': "",
'<': "",
'>': "",
'|': "",
' ': "",
}
func isDot(r rune) bool {
return '.' == r
}
// sanitizeFilename sanitizes a filename for safe use when serving file
func sanitizeFilename(s string) string {
s = strings.ToValidUTF8(s, "")
s = strings.TrimFunc(s, unicode.IsSpace)
s = strings.Map(func(r rune) rune {
if _, ok := filenameSubs[r]; ok {
return -1
}
return r
}, s)
s = strings.TrimLeftFunc(s, isDot)
return s
}

View File

@ -1,21 +1,21 @@
package models
import (
"bytes"
"database/sql"
"git.hoogi.eu/snafu/go-blog/logger"
"strings"
"time"
)
//SQLiteFileDatasource providing an implementation of FileDatasourceService using MariaDB
// SQLiteFileDatasource providing an implementation of FileDatasourceService using MariaDB
type SQLiteFileDatasource struct {
SQLConn *sql.DB
}
//GetByFilename returns the file based on the filename; it the user is given and it is a non admin
//only file specific to this user is returned
func (rdb SQLiteFileDatasource) GetByUniqueName(uniqueName string, u *User) (*File, error) {
var stmt bytes.Buffer
// GetByUniqueName returns the file based on the unique filename; it the user is given and it is a non admin
// only file specific to this user is returned
func (rdb *SQLiteFileDatasource) GetByUniqueName(uniqueName string, u *User) (*File, error) {
var stmt strings.Builder
var args []interface{}
stmt.WriteString("SELECT f.id, f.filename, f.unique_name, f.content_type, f.inline, f.size, f.last_modified, f.user_id, ")
@ -47,11 +47,10 @@ func (rdb SQLiteFileDatasource) GetByUniqueName(uniqueName string, u *User) (*Fi
return &f, nil
}
//Get returns the file based on the filename; it the user is given and it is a non admin
//only file specific to this user is returned
func (rdb SQLiteFileDatasource) Get(fileID int, u *User) (*File, error) {
var stmt bytes.Buffer
// Get returns the file based on the filename; it the user is given and it is a non admin
// only file specific to this user is returned
func (rdb *SQLiteFileDatasource) Get(fileID int, u *User) (*File, error) {
var stmt strings.Builder
var args []interface{}
stmt.WriteString("SELECT f.id, f.filename, f.unique_name, f.content_type, f.inline, f.size, f.last_modified, f.user_id, ")
@ -83,8 +82,8 @@ func (rdb SQLiteFileDatasource) Get(fileID int, u *User) (*File, error) {
return &f, nil
}
//Create inserts some file meta information into the database
func (rdb SQLiteFileDatasource) Create(f *File) (int, error) {
// Create inserts some file meta information into the database
func (rdb *SQLiteFileDatasource) Create(f *File) (int, error) {
res, err := rdb.SQLConn.Exec("INSERT INTO file (filename, unique_name, content_type, inline, size, last_modified, user_id) VALUES(?, ?, ?, ?, ?, ?, ?)",
f.FullFilename, f.UniqueName, f.ContentType, f.Inline, f.Size, time.Now(), f.Author.ID)
@ -101,18 +100,19 @@ func (rdb SQLiteFileDatasource) Create(f *File) (int, error) {
return int(i), nil
}
func (rdb SQLiteFileDatasource) Update(f *File) error {
if _, err := rdb.SQLConn.Exec("UPDATE file SET filename=?, unique_name=?, content_type=?, inline=?, size=?, last_modified=?, user_id=? WHERE id=?", f.FullFilename, f.UniqueName, f.ContentType, f.Inline, f.Size, time.Now(), f.Author.ID, f.ID); err != nil {
func (rdb *SQLiteFileDatasource) Update(f *File) error {
if _, err := rdb.SQLConn.Exec("UPDATE file SET filename=?, unique_name=?, content_type=?, inline=?, size=?, last_modified=?, user_id=? WHERE id=?",
f.FullFilename, f.UniqueName, f.ContentType, f.Inline, f.Size, time.Now(), f.Author.ID, f.ID); err != nil {
return err
}
return nil
}
//List returns a list of files based on the filename; it the user is given and it is a non admin
//only files specific to this user are returned
func (rdb SQLiteFileDatasource) List(u *User, p *Pagination) ([]File, error) {
var stmt bytes.Buffer
// List returns a list of files based on the filename; it the user is given and it is a non admin
// only files specific to this user are returned
func (rdb *SQLiteFileDatasource) List(u *User, p *Pagination) ([]File, error) {
var stmt strings.Builder
var args []interface{}
@ -142,10 +142,13 @@ func (rdb SQLiteFileDatasource) List(u *User, p *Pagination) ([]File, error) {
return nil, err
}
defer rows.Close()
files := []File{}
defer func() {
if err := rows.Close(); err != nil {
logger.Log.Error(err)
}
}()
var files []File
var f File
var us User
@ -168,10 +171,10 @@ func (rdb SQLiteFileDatasource) List(u *User, p *Pagination) ([]File, error) {
}
//Count returns a number of files based on the filename; it the user is given and it is a non admin
//only files specific to this user are counted
func (rdb SQLiteFileDatasource) Count(u *User) (int, error) {
var stmt bytes.Buffer
// Count returns a number of files based on the filename; it the user is given and it is a non admin
// only files specific to this user are counted
func (rdb *SQLiteFileDatasource) Count(u *User) (int, error) {
var stmt strings.Builder
var args []interface{}
@ -193,9 +196,9 @@ func (rdb SQLiteFileDatasource) Count(u *User) (int, error) {
return total, nil
}
//Delete deletes a file based on fileID; users which are not the owner are not allowed to remove files;
//except admins
func (rdb SQLiteFileDatasource) Delete(fileID int) error {
// Delete deletes a file based on fileID; users which are not the owner are not allowed to remove files;
// except admins
func (rdb *SQLiteFileDatasource) Delete(fileID int) error {
if _, err := rdb.SQLConn.Exec("DELETE FROM file WHERE id=?", fileID); err != nil {
return err
}

View File

@ -13,15 +13,11 @@ import (
bf "github.com/russross/blackfriday/v2"
)
// Defines the extensions that are used
var exts = bf.NoIntraEmphasis | bf.Tables | bf.FencedCode | bf.Autolink |
// ext Defines the extensions that are used
var ext = bf.NoIntraEmphasis | bf.Tables | bf.FencedCode | bf.Autolink |
bf.Strikethrough | bf.SpaceHeadings | bf.BackslashLineBreak |
bf.DefinitionLists | bf.Footnotes | bf.HardLineBreak
// Defines the HTML rendering flags that are used
var flags = bf.UseXHTML | bf.Smartypants | bf.SmartypantsFractions |
bf.SmartypantsDashes | bf.SmartypantsLatexDashes | bf.TOC
var p *bluemonday.Policy
func init() {
@ -30,10 +26,10 @@ func init() {
p.AllowAttrs("style").OnElements("span")
}
//MarkdownToHTML sanitizes and parses markdown to HTML
// MarkdownToHTML sanitizes and parses markdown to HTML
func MarkdownToHTML(md []byte) []byte {
md = bytes.Replace(md, []byte("\r\n"), []byte("\n"), -1)
unsafe := bf.Run((md), bf.WithExtensions(exts))
unsafe := bf.Run((md), bf.WithExtensions(ext))
return sanitize(unsafe)
}

View File

@ -3,9 +3,8 @@ package models
import (
"fmt"
"git.hoogi.eu/snafu/go-blog/components/mail"
"git.hoogi.eu/snafu/go-blog/mail"
"git.hoogi.eu/snafu/go-blog/settings"
"git.hoogi.eu/snafu/go-blog/utils"
)
type Mailer struct {
@ -13,36 +12,36 @@ type Mailer struct {
Sender mail.Sender
}
func (m Mailer) SendActivationLink(ui *UserInvite) {
activation := utils.AppendString(m.AppConfig.Domain, "/admin/activate-account/", ui.Hash)
func (m *Mailer) SendActivationLink(ui *UserInvite) {
activation := m.AppConfig.Domain + "/admin/activate-account/" + ui.Hash
mail := mail.Mail{
ml := mail.Mail{
To: ui.Email,
Subject: "You got an invitation",
Body: fmt.Sprintf("Hi %s,\n\n you are invited join %s. To activate your account click the following link and enter a password %s", ui.DisplayName, m.AppConfig.Title, activation),
}
m.Sender.SendAsync(mail)
m.Sender.SendAsync(ml)
}
func (m Mailer) SendPasswordChangeConfirmation(u *User) {
mail := mail.Mail{
func (m *Mailer) SendPasswordChangeConfirmation(u *User) {
ml := mail.Mail{
To: u.Email,
Subject: "Password change",
Body: fmt.Sprintf("Hi %s,\n\nyour password change was successful.", u.DisplayName),
}
m.Sender.SendAsync(mail)
m.Sender.SendAsync(ml)
}
func (m Mailer) SendPasswordResetLink(u *User, t *Token) {
resetLink := utils.AppendString(m.AppConfig.Domain, "/admin/reset-password/", t.Hash)
func (m *Mailer) SendPasswordResetLink(u *User, t *Token) {
resetLink := m.AppConfig.Domain + "/admin/reset-password/" + t.Hash
mail := mail.Mail{
ml := mail.Mail{
To: u.Email,
Subject: "Changing password instructions",
Body: fmt.Sprintf("Hi %s,\n\nuse the following link to reset your password:\n\n%s", u.DisplayName, resetLink),
}
m.Sender.SendAsync(mail)
m.Sender.SendAsync(ml)
}

View File

@ -5,15 +5,13 @@
package models
import (
"bytes"
"fmt"
"html/template"
"math"
"git.hoogi.eu/snafu/go-blog/utils"
"strings"
)
//Pagination type is used to provide a page selector
// Pagination type is used to provide a page selector
type Pagination struct {
Total int
Limit int
@ -21,86 +19,86 @@ type Pagination struct {
RelURL string
}
//Offset returns the offset where to start
func (p Pagination) Offset() int {
// Offset returns the offset where to start
func (p *Pagination) Offset() int {
return (p.CurrentPage - 1) * p.Limit
}
//url returns the absolute url
func (p Pagination) url() string {
// url returns the absolute url
func (p *Pagination) url() string {
if p.RelURL[0] == '/' {
return utils.AppendString(p.RelURL)
return p.RelURL
}
return utils.AppendString("/", p.RelURL)
return "/" + p.RelURL
}
//pages returns the amount of pages
func (p Pagination) pages() int {
// pages returns the amount of pages
func (p *Pagination) pages() int {
return int(math.Ceil(float64(p.Total) / float64(p.Limit)))
}
//hasNext returns true if a next page is available
func (p Pagination) hasNext() bool {
// hasNext returns true if a next page is available
func (p *Pagination) hasNext() bool {
if p.CurrentPage*p.Limit >= p.Total {
return false
}
return true
}
//hasMoreThanOnePage returns true if the bar has more than one page
func (p Pagination) hasMoreThanOnePage() bool {
// hasMoreThanOnePage returns true if the bar has more than one page
func (p *Pagination) hasMoreThanOnePage() bool {
return p.Limit < p.Total
}
//hasPrevious returns true if a previous page is available
func (p Pagination) hasPrevious() bool {
// hasPrevious returns true if a previous page is available
func (p *Pagination) hasPrevious() bool {
return !(p.CurrentPage == 1)
}
//nextPage returns the next page
func (p Pagination) nextPage() int {
// nextPage returns the next page
func (p *Pagination) nextPage() int {
if !p.hasNext() {
return p.CurrentPage
}
return p.CurrentPage + 1
}
//previousPage returns the previous page
func (p Pagination) previousPage() int {
// previousPage returns the previous page
func (p *Pagination) previousPage() int {
if !p.hasPrevious() {
return p.CurrentPage
}
return p.CurrentPage - 1
}
//PaginationBar returns the HTML for the pagination bar which can be embedded
func (p Pagination) PaginationBar() template.HTML {
var buffer bytes.Buffer
// PaginationBar returns the HTML for the pagination bar which can be embedded
func (p *Pagination) PaginationBar() template.HTML {
var sb strings.Builder
if p.pages() > 1 {
buffer.WriteString(`<div id="pagination">`)
sb.WriteString(`<div id="pagination">`)
if !p.hasPrevious() {
buffer.WriteString(`<a class="button button-inactive" href="#">&laquo; Backward</a>`)
sb.WriteString(`<a class="button button-inactive" href="#">&laquo; Backward</a>`)
} else {
buffer.WriteString(fmt.Sprintf(`<a class="button button-active" href="%s/%d">&laquo; Backward</a>`, p.url(), p.previousPage()))
sb.WriteString(fmt.Sprintf(`<a class="button button-active" href="%s/%d">&laquo; Backward</a>`, p.url(), p.previousPage()))
}
for i := 1; i <= p.pages(); i++ {
if p.CurrentPage == i {
buffer.WriteString(fmt.Sprintf(`<a class="button button-inactive" href="#">%d</a></li>`, i))
sb.WriteString(fmt.Sprintf(`<a class="button button-inactive" href="#">%d</a>`, i))
} else {
buffer.WriteString(fmt.Sprintf(`<a class="button button-active" href="%s/%d">%d</a></li>`, p.url(), i, i))
sb.WriteString(fmt.Sprintf(`<a class="button button-active" href="%s/%d">%d</a>`, p.url(), i, i))
}
}
if !p.hasNext() {
buffer.WriteString(`<a class="button button-inactive" href="#">Forward &raquo;</a>`)
sb.WriteString(`<a class="button button-inactive" href="#">Forward &raquo;</a>`)
} else {
buffer.WriteString(fmt.Sprintf(`<a class="button button-active" href="%s/%d">Forward &raquo;</a>`, p.url(), p.nextPage()))
sb.WriteString(fmt.Sprintf(`<a class="button button-active" href="%s/%d">Forward &raquo;</a>`, p.url(), p.nextPage()))
}
buffer.WriteString(`</div>`)
sb.WriteString(`</div>`)
}
return template.HTML(buffer.String())
return template.HTML(sb.String())
}

View File

@ -7,11 +7,11 @@ import (
"net/url"
"time"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/utils"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/slug"
)
//SiteDatasourceService defines an interface for CRUD operations on sites
// SiteDatasourceService defines an interface for CRUD operations on sites
type SiteDatasourceService interface {
Create(s *Site) (int, error)
List(pc PublishedCriteria, p *Pagination) ([]Site, error)
@ -25,17 +25,17 @@ type SiteDatasourceService interface {
Count(pc PublishedCriteria) (int, error)
}
//Direction type to distinct if a site should be moved up or down
// Direction type to distinct if a site should be moved up or down
type Direction int
const (
//Up for moving the site one up
// Up for moving the site one up
Up = iota
//Down for moving the site one down
// Down for moving the site one down
Down
)
//Site represents a site
// Site represents a site
type Site struct {
ID int
Title string
@ -50,21 +50,21 @@ type Site struct {
}
// LinkEscape escapes a link for safe use in URLs
func (s Site) LinkEscape() string {
func (s *Site) LinkEscape() string {
if s.isExternal() {
return s.Link
}
return utils.AppendString("/site/", url.PathEscape(s.Link))
return "/site/" + url.PathEscape(s.Link)
}
func (s Site) safeLink() string {
func (s *Site) safeLink() string {
if s.isExternal() {
return s.Link
}
return utils.CreateURLSafeSlug(s.Link, -1)
return slug.CreateURLSafeSlug(s.Link, -1)
}
func (s Site) isExternal() bool {
func (s *Site) isExternal() bool {
if len(s.Link) > 6 {
if s.Link[:7] == "http://" {
return true
@ -114,35 +114,29 @@ func (s *Site) validate(ds SiteDatasourceService, changeLink bool) error {
return nil
}
//SiteService containing the service to access site
// SiteService containing the service to access site
type SiteService struct {
Datasource SiteDatasourceService
}
//List returns all sites
func (ss SiteService) List(pc PublishedCriteria, p *Pagination) ([]Site, error) {
// List returns all sites
func (ss *SiteService) List(pc PublishedCriteria, p *Pagination) ([]Site, error) {
return ss.Datasource.List(pc, p)
}
//Publish switches the publish state of the site
func (ss SiteService) Publish(siteID int) error {
// Publish switches the publish state of the site
func (ss *SiteService) Publish(siteID int) error {
s, err := ss.Datasource.Get(siteID, All)
if err != nil {
return err
}
err = ss.Datasource.Publish(s)
if err != nil {
return err
}
return nil
return ss.Datasource.Publish(s)
}
//Create creates a site
func (ss SiteService) Create(s *Site) (int, error) {
// Create creates a site
func (ss *SiteService) Create(s *Site) (int, error) {
if err := s.validate(ss.Datasource, true); err != nil {
return -1, err
}
@ -160,13 +154,13 @@ func (ss SiteService) Create(s *Site) (int, error) {
return ss.Datasource.Create(s)
}
//Order reorder the site
func (ss SiteService) Order(siteID int, dir Direction) error {
// Order reorder the site
func (ss *SiteService) Order(siteID int, dir Direction) error {
return ss.Datasource.Order(siteID, dir)
}
//Update updates a site
func (ss SiteService) Update(s *Site) error {
// Update updates a site
func (ss *SiteService) Update(s *Site) error {
oldSite, err := ss.GetByID(s.ID, All)
if err != nil {
@ -174,6 +168,7 @@ func (ss SiteService) Update(s *Site) error {
}
changeLink := false
if oldSite.Link != s.Link {
changeLink = true
s.Link = s.safeLink()
@ -186,8 +181,8 @@ func (ss SiteService) Update(s *Site) error {
return ss.Datasource.Update(s)
}
//Delete deletes a site
func (ss SiteService) Delete(siteID int) error {
// Delete deletes a site
func (ss *SiteService) Delete(siteID int) error {
s, err := ss.GetByID(siteID, All)
if err != nil {
@ -198,18 +193,18 @@ func (ss SiteService) Delete(siteID int) error {
}
// GetByLink Get a site by the link.
func (ss SiteService) GetByLink(link string, pc PublishedCriteria) (*Site, error) {
func (ss *SiteService) GetByLink(link string, pc PublishedCriteria) (*Site, error) {
return ss.Datasource.GetByLink(link, pc)
}
// GetByID Get a site by the id.
func (ss SiteService) GetByID(siteID int, pc PublishedCriteria) (*Site, error) {
func (ss *SiteService) GetByID(siteID int, pc PublishedCriteria) (*Site, error) {
return ss.Datasource.Get(siteID, pc)
}
// Count returns the number of sites
func (ss SiteService) Count(pc PublishedCriteria) (int, error) {
func (ss *SiteService) Count(pc PublishedCriteria) (int, error) {
return ss.Datasource.Count(pc)
}

View File

@ -1,22 +1,21 @@
package models
import (
"bytes"
"database/sql"
"strings"
"time"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/logger"
)
//SQLiteSiteDatasource providing an implementation of SiteDatasourceService for sqlite
// SQLiteSiteDatasource providing an implementation of SiteDatasourceService for sqlite
type SQLiteSiteDatasource struct {
SQLConn *sql.DB
}
//List returns a array of sites
func (rdb SQLiteSiteDatasource) List(pc PublishedCriteria, p *Pagination) ([]Site, error) {
var stmt bytes.Buffer
// List returns a array of sites
func (rdb *SQLiteSiteDatasource) List(pc PublishedCriteria, p *Pagination) ([]Site, error) {
var stmt strings.Builder
var args []interface{}
stmt.WriteString("SELECT s.id, s.title, s.link, s.section, s.content, s.published, s.published_on, s.last_modified, s.order_no, u.id, u.display_name, u.email, u.username ")
@ -40,14 +39,18 @@ func (rdb SQLiteSiteDatasource) List(pc PublishedCriteria, p *Pagination) ([]Sit
}
rows, err := rdb.SQLConn.Query(stmt.String(), args...)
if err != nil {
return nil, err
}
defer rows.Close()
sites := []Site{}
defer func() {
if err := rows.Close(); err != nil {
logger.Log.Error(err)
}
}()
var sites []Site
var s Site
var u User
@ -68,9 +71,9 @@ func (rdb SQLiteSiteDatasource) List(pc PublishedCriteria, p *Pagination) ([]Sit
return sites, nil
}
//Get returns a site based on the site id
func (rdb SQLiteSiteDatasource) Get(siteID int, pc PublishedCriteria) (*Site, error) {
var stmt bytes.Buffer
// Get returns a site based on the site id
func (rdb *SQLiteSiteDatasource) Get(siteID int, pc PublishedCriteria) (*Site, error) {
var stmt strings.Builder
var args []interface{}
stmt.WriteString("SELECT s.id, s.title, s.link, s.section, s.content, s.published, s.published_on, s.last_modified, s.order_no, u.id, u.display_name, u.email, u.username FROM site as s ")
@ -97,9 +100,9 @@ func (rdb SQLiteSiteDatasource) Get(siteID int, pc PublishedCriteria) (*Site, er
return &s, nil
}
//GetByLink returns a site based on the provided link
func (rdb SQLiteSiteDatasource) GetByLink(link string, pc PublishedCriteria) (*Site, error) {
var stmt bytes.Buffer
// GetByLink returns a site based on the provided link
func (rdb *SQLiteSiteDatasource) GetByLink(link string, pc PublishedCriteria) (*Site, error) {
var stmt strings.Builder
var args []interface{}
stmt.WriteString("SELECT s.id, s.title, s.link, s.section, s.content, s.published, s.published_on, s.order_no, s.last_modified, u.id, u.display_name, u.email, u.username FROM site as s ")
@ -117,7 +120,8 @@ func (rdb SQLiteSiteDatasource) GetByLink(link string, pc PublishedCriteria) (*S
var s Site
var u User
if err := rdb.SQLConn.QueryRow(stmt.String(), link).Scan(&s.ID, &s.Title, &s.Link, &s.Section, &s.Content, &s.Published, &s.PublishedOn, &s.OrderNo, &s.LastModified, &u.ID, &u.DisplayName, &u.Email, &u.Username); err != nil {
if err := rdb.SQLConn.QueryRow(stmt.String(), link).Scan(&s.ID, &s.Title, &s.Link, &s.Section, &s.Content, &s.Published,
&s.PublishedOn, &s.OrderNo, &s.LastModified, &u.ID, &u.DisplayName, &u.Email, &u.Username); err != nil {
return nil, err
}
@ -126,9 +130,10 @@ func (rdb SQLiteSiteDatasource) GetByLink(link string, pc PublishedCriteria) (*S
return &s, nil
}
//Publish publishes or unpublishes a site
func (rdb SQLiteSiteDatasource) Publish(s *Site) error {
// Publish publishes or unpublishes a site
func (rdb *SQLiteSiteDatasource) Publish(s *Site) error {
publishOn := NullTime{Valid: false}
if !s.Published {
publishOn = NullTime{Time: time.Now(), Valid: true}
}
@ -139,9 +144,10 @@ func (rdb SQLiteSiteDatasource) Publish(s *Site) error {
return nil
}
//Create creates a site
func (rdb SQLiteSiteDatasource) Create(s *Site) (int, error) {
res, err := rdb.SQLConn.Exec("INSERT INTO site (title, link, section, content, published, published_on, last_modified, order_no, user_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)",
// Create creates a site
func (rdb *SQLiteSiteDatasource) Create(s *Site) (int, error) {
res, err := rdb.SQLConn.Exec("INSERT INTO site (title, link, section, content, published, published_on, last_modified, order_no, user_id) "+
"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)",
s.Title, s.Link, s.Section, s.Content, s.Published, s.PublishedOn, time.Now(), s.OrderNo, s.Author.ID)
if err != nil {
@ -157,8 +163,8 @@ func (rdb SQLiteSiteDatasource) Create(s *Site) (int, error) {
return int(i), nil
}
//Order moves a site up or down
func (rdb SQLiteSiteDatasource) Order(id int, d Direction) error {
// Order moves a site up or down
func (rdb *SQLiteSiteDatasource) Order(id int, d Direction) error {
tx, err := rdb.SQLConn.Begin()
if err != nil {
@ -168,14 +174,15 @@ func (rdb SQLiteSiteDatasource) Order(id int, d Direction) error {
defer func() {
if err != nil {
logger.Log.Error("error during ordering of sites ", err)
tx.Rollback()
err := tx.Rollback()
if err != nil {
logger.Log.Error("error during transaction rollback ", err)
}
}
}()
if err != nil {
return err
}
if d == Up {
if _, err = tx.Exec("UPDATE site "+
"SET order_no=(SELECT order_no AS order_no FROM site WHERE id=?) "+
@ -189,7 +196,9 @@ func (rdb SQLiteSiteDatasource) Order(id int, d Direction) error {
} else if d == Down {
var max int
tx.QueryRow("SELECT MAX(order_no) AS max FROM site").Scan(&max)
if err := tx.QueryRow("SELECT MAX(order_no) AS max FROM site").Scan(&max); err != nil {
return err
}
if _, err = tx.Exec("UPDATE site "+
"SET order_no=(SELECT order_no AS swap_el FROM site WHERE id=?) "+
@ -205,27 +214,22 @@ func (rdb SQLiteSiteDatasource) Order(id int, d Direction) error {
}
}
err = tx.Commit()
return tx.Commit()
}
if err != nil {
// Update updates a site
func (rdb *SQLiteSiteDatasource) Update(s *Site) error {
if _, err := rdb.SQLConn.Exec("UPDATE site SET title=?, link=?, section=?, content=?, last_modified=? WHERE id=?",
s.Title, s.Link, s.Section, s.Content, time.Now(), s.ID); err != nil {
return err
}
return nil
}
//Update updates a site
func (rdb SQLiteSiteDatasource) Update(s *Site) error {
if _, err := rdb.SQLConn.Exec("UPDATE site SET title=?, link=?, section=?, content=?, last_modified=? WHERE id=?", s.Title, s.Link, s.Section, s.Content, time.Now(), s.ID); err != nil {
return err
}
return nil
}
//Count returns the amount of sites
func (rdb SQLiteSiteDatasource) Count(pc PublishedCriteria) (int, error) {
var stmt bytes.Buffer
// Count returns the amount of sites
func (rdb *SQLiteSiteDatasource) Count(pc PublishedCriteria) (int, error) {
var stmt strings.Builder
stmt.WriteString("SELECT count(id) FROM site ")
@ -244,8 +248,8 @@ func (rdb SQLiteSiteDatasource) Count(pc PublishedCriteria) (int, error) {
return total, nil
}
//Max returns the maximum order number
func (rdb SQLiteSiteDatasource) Max() (int, error) {
// Max returns the maximum order number
func (rdb *SQLiteSiteDatasource) Max() (int, error) {
var max sql.NullInt64
if err := rdb.SQLConn.QueryRow("SELECT MAX(order_no) FROM site").Scan(&max); err != nil {
@ -259,8 +263,8 @@ func (rdb SQLiteSiteDatasource) Max() (int, error) {
return int(max.Int64), nil
}
//Delete deletes a site and updates the order numbers
func (rdb SQLiteSiteDatasource) Delete(s *Site) error {
// Delete deletes a site and updates the order numbers
func (rdb *SQLiteSiteDatasource) Delete(s *Site) error {
tx, err := rdb.SQLConn.Begin()
if err != nil {
@ -269,8 +273,11 @@ func (rdb SQLiteSiteDatasource) Delete(s *Site) error {
defer func() {
if err != nil {
logger.Log.Error("error during delete transaction", err)
tx.Rollback()
logger.Log.Errorf("error site removal not successful %v", err)
if err := tx.Rollback(); err != nil {
logger.Log.Errorf("could not rollback transaction during site removal %v", err)
return
}
return
}
}()
@ -283,9 +290,7 @@ func (rdb SQLiteSiteDatasource) Delete(s *Site) error {
return err
}
err = tx.Commit()
if err != nil {
if err = tx.Commit(); err != nil {
return err
}

View File

@ -5,15 +5,15 @@ import (
"time"
)
//AdminCriteria specifies which type of users should be considered
// AdminCriteria specifies which type of users should be considered
type AdminCriteria int
const (
//OnlyAdmins conider only published
// OnlyAdmins consider only admins
OnlyAdmins = iota
//NoAdmins conider no admins
// NoAdmins consider no admins
NoAdmins
//AllUser conider all users
// AllUser consider all users
AllUser
)
@ -21,15 +21,15 @@ const (
type PublishedCriteria int
const (
// OnlyPublished conider only published
// OnlyPublished consider only published
OnlyPublished = iota
// NotPublished conider only not published
// NotPublished consider only not published
NotPublished
// All conider both published and not published
// All consider both published and not published
All
)
//NullTime reprensents a time which may not valid if time is null
// NullTime represents a time which may not valid if time is null
type NullTime struct {
Time time.Time
Valid bool

View File

@ -7,12 +7,12 @@ import (
"net/http"
"time"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/utils"
"git.hoogi.eu/snafu/go-blog/crypt"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/logger"
)
//TokenDatasourceService defines an interface for CRUD operations for tokens
// TokenDatasourceService defines an interface for CRUD operations for tokens
type TokenDatasourceService interface {
Create(t *Token) (int, error)
Get(hash string, tt TokenType) (*Token, error)
@ -20,7 +20,7 @@ type TokenDatasourceService interface {
Remove(hash string, tt TokenType) error
}
//Token represents a token
// Token represents a token
type Token struct {
ID int
Hash string
@ -31,19 +31,19 @@ type Token struct {
}
const (
//PasswordReset token generated for resetting passwords
// PasswordReset token generated for resetting passwords
PasswordReset = iota
)
var types = [...]string{"password_reset"}
//TokenType specifies the type where token can be used
// TokenType specifies the type where token can be used
type TokenType int
// Scan implements the Scanner interface.
func (tt *TokenType) Scan(value interface{}) error {
for k, t := range types {
if t == string(value.([]byte)) {
if t == (value.(string)) {
tts := TokenType(k)
tt = &tts
return nil
@ -61,23 +61,25 @@ func (tt TokenType) String() string {
return types[tt]
}
//TokenService containing the service to access tokens
// TokenService containing the service to access tokens
type TokenService struct {
Datasource TokenDatasourceService
}
//Create creates a new token
func (ts TokenService) Create(t *Token) error {
t.Hash = utils.RandomHash(32)
// Create creates a new token
func (ts *TokenService) Create(t *Token) error {
t.Hash = crypt.RandomHash(32)
_, err := ts.Datasource.Create(t)
if _, err := ts.Datasource.Create(t); err != nil {
return err
}
return err
return nil
}
//Get token for a defined token type expires after a defined time
//Expired token will be removed
func (ts TokenService) Get(hash string, tt TokenType, expireAfter time.Duration) (*Token, error) {
// Get token for a defined token type expires after a defined time
// Expired token will be removed
func (ts *TokenService) Get(hash string, tt TokenType, expireAfter time.Duration) (*Token, error) {
token, err := ts.Datasource.Get(hash, tt)
if err != nil {
@ -96,8 +98,8 @@ func (ts TokenService) Get(hash string, tt TokenType, expireAfter time.Duration)
return token, nil
}
//RateLimit returns an error if a token is requested greater three times in a time span of 15 minutes
func (ts TokenService) RateLimit(userID int, tt TokenType) error {
// RateLimit returns an error if a token is requested greater three times in a time span of 15 minutes
func (ts *TokenService) RateLimit(userID int, tt TokenType) error {
tokens, err := ts.Datasource.ListByUser(userID, tt)
if err != nil {
@ -106,7 +108,7 @@ func (ts TokenService) RateLimit(userID int, tt TokenType) error {
now := time.Now()
rate := []Token{}
var rate []Token
for _, t := range tokens {
if now.Sub(t.RequestedAt) < time.Minute*15 {
rate = append(rate, t)
@ -120,7 +122,7 @@ func (ts TokenService) RateLimit(userID int, tt TokenType) error {
return nil
}
//Remove removes a token
func (ts TokenService) Remove(hash string, tt TokenType) error {
// Remove removes a token
func (ts *TokenService) Remove(hash string, tt TokenType) error {
return ts.Datasource.Remove(hash, tt)
}

View File

@ -2,16 +2,17 @@ package models
import (
"database/sql"
"git.hoogi.eu/snafu/go-blog/logger"
"time"
)
//SQLiteTokenDatasource providing an implementation of TokenDatasourceService using MariaDB
// SQLiteTokenDatasource providing an implementation of TokenDatasourceService using MariaDB
type SQLiteTokenDatasource struct {
SQLConn *sql.DB
}
//Create creates a new token
func (rdb SQLiteTokenDatasource) Create(t *Token) (int, error) {
// Create creates a new token
func (rdb *SQLiteTokenDatasource) Create(t *Token) (int, error) {
res, err := rdb.SQLConn.Exec("INSERT INTO token (hash, requested_at, token_type, user_id) VALUES(?, ?, ?, ?)",
t.Hash, time.Now(), t.Type, t.Author.ID)
@ -28,8 +29,8 @@ func (rdb SQLiteTokenDatasource) Create(t *Token) (int, error) {
return int(i), nil
}
//Get gets a token based on the hash and the token type
func (rdb SQLiteTokenDatasource) Get(hash string, tt TokenType) (*Token, error) {
// Get gets a token based on the hash and the token type
func (rdb *SQLiteTokenDatasource) Get(hash string, tt TokenType) (*Token, error) {
var t Token
var u User
@ -43,15 +44,19 @@ func (rdb SQLiteTokenDatasource) Get(hash string, tt TokenType) (*Token, error)
return &t, nil
}
//ListByUser receives all tokens based on the user id and the token type ordered by requested
func (rdb SQLiteTokenDatasource) ListByUser(userID int, tt TokenType) ([]Token, error) {
// ListByUser receives all tokens based on the user id and the token type ordered by requested
func (rdb *SQLiteTokenDatasource) ListByUser(userID int, tt TokenType) ([]Token, error) {
rows, err := rdb.SQLConn.Query("SELECT t.id, t.hash, t.requested_at, t.token_type, t.user_id FROM token as t WHERE t.user_id=? AND t.token_type=? ", userID, tt.String())
if err != nil {
return nil, err
}
defer rows.Close()
defer func() {
if err := rows.Close(); err != nil {
logger.Log.Error(err)
}
}()
tokens := []Token{}
@ -70,8 +75,8 @@ func (rdb SQLiteTokenDatasource) ListByUser(userID int, tt TokenType) ([]Token,
return tokens, nil
}
//Remove removes a token based on the hash
func (rdb SQLiteTokenDatasource) Remove(hash string, tt TokenType) error {
// Remove removes a token based on the hash
func (rdb *SQLiteTokenDatasource) Remove(hash string, tt TokenType) error {
if _, err := rdb.SQLConn.Exec("DELETE FROM token WHERE hash=? AND token_type=? ", hash, tt.String()); err != nil {
return err
}

View File

@ -12,14 +12,14 @@ import (
"strings"
"time"
"git.hoogi.eu/snafu/go-blog/components/httperror"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/crypt"
"git.hoogi.eu/snafu/go-blog/httperror"
"git.hoogi.eu/snafu/go-blog/logger"
"git.hoogi.eu/snafu/go-blog/settings"
"git.hoogi.eu/snafu/go-blog/utils"
"golang.org/x/crypto/bcrypt"
)
//UserDatasourceService defines an interface for CRUD operations for users
// UserDatasourceService defines an interface for CRUD operations for users
type UserDatasourceService interface {
Create(u *User) (int, error)
List(p *Pagination) ([]User, error)
@ -31,7 +31,7 @@ type UserDatasourceService interface {
Remove(userID int) error
}
//User represents a user
// User represents a user
type User struct {
ID int
Username string
@ -45,18 +45,14 @@ type User struct {
IsAdmin bool
}
const (
bcryptRounds = 12
)
//UserService containing the service to access users
// UserService containing the service to access users
type UserService struct {
Datasource UserDatasourceService
Config settings.User
UserInterceptor UserInterceptor
}
//UserInterceptor will be executed before and after updating/creating users
// UserInterceptor will be executed before and after updating/creating users
type UserInterceptor interface {
PreCreate(user *User) error
PostCreate(user *User) error
@ -74,7 +70,7 @@ const (
VPassword
)
func (u *User) validate(us UserService, minPasswordLength int, v Validations) error {
func (u *User) validate(us *UserService, minPasswordLength int, v Validations) error {
u.DisplayName = strings.TrimSpace(u.DisplayName)
u.Email = strings.TrimSpace(u.Email)
u.Username = strings.TrimSpace(u.Username)
@ -113,17 +109,13 @@ func (u *User) validate(us UserService, minPasswordLength int, v Validations) er
}
if (v & VDupEmail) != 0 {
err := us.duplicateMail(u.Email)
if err != nil {
if err := us.duplicateMail(u.Email); err != nil {
return err
}
}
if (v & VDupUsername) != 0 {
err := us.duplicateUsername(u.Username)
if err != nil {
if err := us.duplicateUsername(u.Username); err != nil {
return err
}
}
@ -131,10 +123,11 @@ func (u *User) validate(us UserService, minPasswordLength int, v Validations) er
return nil
}
func (us UserService) duplicateMail(mail string) error {
func (us *UserService) duplicateMail(mail string) error {
user, err := us.Datasource.GetByMail(mail)
if err != nil {
if err != sql.ErrNoRows {
if !errors.Is(err, sql.ErrNoRows) {
return err
}
}
@ -146,13 +139,15 @@ func (us UserService) duplicateMail(mail string) error {
return nil
}
func (us UserService) duplicateUsername(username string) error {
func (us *UserService) duplicateUsername(username string) error {
user, err := us.Datasource.GetByUsername(username)
if err != nil {
if err != sql.ErrNoRows {
if !errors.Is(err, sql.ErrNoRows) {
return err
}
}
if user != nil {
return httperror.New(http.StatusUnprocessableEntity,
fmt.Sprintf("The username %s already exists.", username),
@ -162,21 +157,22 @@ func (us UserService) duplicateUsername(username string) error {
return nil
}
//Count returns the amount of users
func (us UserService) Count(a AdminCriteria) (int, error) {
// Count returns the amount of users
func (us *UserService) Count(a AdminCriteria) (int, error) {
return us.Datasource.Count(a)
}
//List returns a list of users. Limits the amount based on the defined pagination
func (us UserService) List(p *Pagination) ([]User, error) {
// List returns a list of users. Limits the amount based on the defined pagination
func (us *UserService) List(p *Pagination) ([]User, error) {
return us.Datasource.List(p)
}
//GetByID gets the user based on the given id; will not contain the user password
func (us UserService) GetByID(userID int) (*User, error) {
// GetByID gets the user based on the given id; will not contain the user password
func (us *UserService) GetByID(userID int) (*User, error) {
u, err := us.Datasource.Get(userID)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return nil, httperror.NotFound("user", fmt.Errorf("the user with id %d was not found", userID))
}
return nil, err
@ -185,11 +181,12 @@ func (us UserService) GetByID(userID int) (*User, error) {
return u, nil
}
//GetByUsername gets the user based on the given username; will contain the user password
func (us UserService) GetByUsername(username string) (*User, error) {
// GetByUsername gets the user based on the given username; will contain the user password
func (us *UserService) GetByUsername(username string) (*User, error) {
u, err := us.Datasource.GetByUsername(username)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return nil, httperror.NotFound("user", err)
}
return nil, err
@ -198,12 +195,12 @@ func (us UserService) GetByUsername(username string) (*User, error) {
return u, nil
}
//GetByMail gets the user based on the given mail; will contain the user password
func (us UserService) GetByMail(mail string) (*User, error) {
// GetByMail gets the user based on the given mail; will contain the user password
func (us *UserService) GetByMail(mail string) (*User, error) {
u, err := us.Datasource.GetByMail(mail)
if err != nil {
if err == sql.ErrNoRows {
if errors.Is(err, sql.ErrNoRows) {
return nil, httperror.NotFound("user", err)
}
@ -212,9 +209,9 @@ func (us UserService) GetByMail(mail string) (*User, error) {
return u, nil
}
//Create creates the user
//If an UserInterceptor is available the action PreCreate is executed before creating and PostCreate after creating the user
func (us UserService) Create(u *User) (int, error) {
// Create creates the user
// If an UserInterceptor is available the action PreCreate is executed before creating and PostCreate after creating the user
func (us *UserService) Create(u *User) (int, error) {
if us.UserInterceptor != nil {
if err := us.UserInterceptor.PreCreate(u); err != nil {
return -1, httperror.InternalServerError(fmt.Errorf("error while executing user interceptor 'PreCreate' error %v", err))
@ -225,9 +222,9 @@ func (us UserService) Create(u *User) (int, error) {
return -1, err
}
salt := utils.GenerateSalt()
saltedPassword := utils.AppendBytes(u.PlainPassword, salt)
password, err := utils.CryptPassword([]byte(saltedPassword), bcryptRounds)
salt := crypt.GenerateSalt()
saltedPassword := append(u.PlainPassword[:], salt[:]...)
password, err := crypt.CryptPassword([]byte(saltedPassword))
if err != nil {
return -1, err
@ -256,7 +253,7 @@ func (us UserService) Create(u *User) (int, error) {
//Update updates the user
//If an UserInterceptor is available the action PreUpdate is executed before updating and PostUpdate after updating the user
func (us UserService) Update(u *User, changePassword bool) error {
func (us *UserService) Update(u *User, changePassword bool) error {
oldUser, err := us.Datasource.Get(u.ID)
if err != nil {
@ -270,7 +267,6 @@ func (us UserService) Update(u *User, changePassword bool) error {
}
if us.UserInterceptor != nil {
if err := us.UserInterceptor.PreUpdate(oldUser, u); err != nil {
return httperror.InternalServerError(fmt.Errorf("error while executing user interceptor 'PreUpdate' error %v", err))
}
@ -309,9 +305,9 @@ func (us UserService) Update(u *User, changePassword bool) error {
}
if changePassword {
salt := utils.GenerateSalt()
saltedPassword := utils.AppendBytes(u.PlainPassword, salt)
password, err := utils.CryptPassword([]byte(saltedPassword), bcryptRounds)
salt := crypt.GenerateSalt()
saltedPassword := append(u.PlainPassword[:], salt[:]...)
password, err := crypt.CryptPassword([]byte(saltedPassword))
if err != nil {
return err
@ -321,7 +317,9 @@ func (us UserService) Update(u *User, changePassword bool) error {
u.Salt = salt
}
err = us.Datasource.Update(u, changePassword)
if err = us.Datasource.Update(u, changePassword); err != nil {
return err
}
u.Password = nil
@ -330,14 +328,15 @@ func (us UserService) Update(u *User, changePassword bool) error {
logger.Log.Errorf("error while executing PostUpdate user interceptor method %v", err)
}
}
u.PlainPassword = nil
return err
return nil
}
// Authenticate authenticates the user by the given login method (email or username)
// if the user was found but the password is wrong the found user and an error will be returned
func (us UserService) Authenticate(u *User, loginMethod settings.LoginMethod) (*User, error) {
func (us *UserService) Authenticate(u *User, loginMethod settings.LoginMethod) (*User, error) {
var err error
if len(u.Username) == 0 || len(u.PlainPassword) == 0 {
@ -353,9 +352,9 @@ func (us UserService) Authenticate(u *User, loginMethod settings.LoginMethod) (*
}
if err != nil {
if err == sql.ErrNoRows {
//Do some extra work
bcrypt.CompareHashAndPassword([]byte("$2a$12$bQlRnXTNZMp6kCyoAlnf3uZW5vtmSj9CHP7pYplRUVK2n0C5xBHBa"), password)
//Do some extra work
bcrypt.CompareHashAndPassword([]byte("$2a$12$bQlRnXTNZMp6kCyoAlnf3uZW5vtmSj9CHP7pYplRUVK2n0C5xBHBa"), password)
if errors.Is(err, sql.ErrNoRows) {
return nil, httperror.New(http.StatusUnauthorized, "Your username or password is invalid.", err)
}
return nil, err
@ -381,7 +380,7 @@ func (us UserService) Authenticate(u *User, loginMethod settings.LoginMethod) (*
}
// Remove removes the user returns an error if no administrator would remain
func (us UserService) Remove(u *User) error {
func (us *UserService) Remove(u *User) error {
if us.UserInterceptor != nil {
if err := us.UserInterceptor.PreRemove(u); err != nil {
return httperror.InternalServerError(fmt.Errorf("error while executing user interceptor 'PreRemove' error %v", err))
@ -413,8 +412,8 @@ func (us UserService) Remove(u *User) error {
return err
}
//OneAdmin returns true if there is only one admin
func (us UserService) OneAdmin() (bool, error) {
// OneAdmin returns true if there is only one admin
func (us *UserService) OneAdmin() (bool, error) {
c, err := us.Datasource.Count(OnlyAdmins)
if err != nil {
@ -428,6 +427,6 @@ func (us UserService) OneAdmin() (bool, error) {
return false, nil
}
func (u User) comparePassword() error {
return bcrypt.CompareHashAndPassword(u.Password, utils.AppendBytes(u.PlainPassword, u.Salt))
func (u *User) comparePassword() error {
return bcrypt.CompareHashAndPassword(u.Password, append(u.PlainPassword[:], u.Salt[:]...))
}

View File

@ -3,11 +3,12 @@ package models
import (
"time"
"git.hoogi.eu/snafu/go-blog/components/mail"
"git.hoogi.eu/snafu/go-blog/utils"
"git.hoogi.eu/snafu/go-blog/crypt"
"git.hoogi.eu/snafu/go-blog/mail"
)
//User represents a user
// TODO: refactor
// UserInvite represents a new invited user
type UserInvite struct {
ID int
Hash string
@ -29,7 +30,7 @@ func (ui UserInvite) Copy() *User {
}
}
//UserInviteDatasourceService defines an interface for CRUD operations for users
// UserInviteDatasourceService defines an interface for CRUD operations for users
type UserInviteDatasourceService interface {
List() ([]UserInvite, error)
Get(inviteID int) (*UserInvite, error)
@ -39,32 +40,26 @@ type UserInviteDatasourceService interface {
Remove(inviteID int) error
}
//UserInviteService
// UserInviteService
type UserInviteService struct {
Datasource UserInviteDatasourceService
UserService UserService
MailService mail.Service
UserService *UserService
MailService *mail.Service
}
// validate A user invitation must conform the user validations except the password checks
func (ui UserInvite) validate(uis UserInviteService) error {
func (ui UserInvite) validate(uis *UserInviteService) error {
user := ui.Copy()
err := user.validate(uis.UserService, -1, VDupEmail|VDupUsername)
if err != nil {
return err
}
return nil
return user.validate(uis.UserService, -1, VDupEmail|VDupUsername)
}
func (uis UserInviteService) List() ([]UserInvite, error) {
func (uis *UserInviteService) List() ([]UserInvite, error) {
return uis.Datasource.List()
}
func (uis UserInviteService) Update(ui *UserInvite) error {
ui.Hash = utils.RandomHash(32)
func (uis *UserInviteService) Update(ui *UserInvite) error {
ui.Hash = crypt.RandomHash(32)
if err := ui.validate(uis); err != nil {
return err
@ -73,8 +68,8 @@ func (uis UserInviteService) Update(ui *UserInvite) error {
return uis.Datasource.Update(ui)
}
func (uis UserInviteService) Create(ui *UserInvite) (int, error) {
ui.Hash = utils.RandomHash(32)
func (uis *UserInviteService) Create(ui *UserInvite) (int, error) {
ui.Hash = crypt.RandomHash(32)
if err := ui.validate(uis); err != nil {
return -1, err
@ -83,14 +78,14 @@ func (uis UserInviteService) Create(ui *UserInvite) (int, error) {
return uis.Datasource.Create(ui)
}
func (uis UserInviteService) Get(inviteID int) (*UserInvite, error) {
func (uis *UserInviteService) Get(inviteID int) (*UserInvite, error) {
return uis.Datasource.Get(inviteID)
}
func (uis UserInviteService) GetByHash(hash string) (*UserInvite, error) {
func (uis *UserInviteService) GetByHash(hash string) (*UserInvite, error) {
return uis.Datasource.GetByHash(hash)
}
func (uis UserInviteService) Remove(inviteID int) error {
func (uis *UserInviteService) Remove(inviteID int) error {
return uis.Datasource.Remove(inviteID)
}

View File

@ -1,39 +1,33 @@
package models
import (
"bytes"
"database/sql"
"git.hoogi.eu/snafu/go-blog/logger"
"time"
)
//SQLiteUserInviteDatasource
// SQLiteUserInviteDatasource
type SQLiteUserInviteDatasource struct {
SQLConn *sql.DB
}
func (rdb SQLiteUserInviteDatasource) List() ([]UserInvite, error) {
var stmt bytes.Buffer
var args []interface{}
func (rdb *SQLiteUserInviteDatasource) List() ([]UserInvite, error) {
var invites []UserInvite
var ui UserInvite
var u User
stmt.WriteString("SELECT ui.id, ui.username, ui.email, ui.display_name, ui.created_at, ui.is_admin, ")
stmt.WriteString("u.id, u.username, u.email, u.display_name ")
stmt.WriteString("FROM user_invite as ui ")
stmt.WriteString("INNER JOIN user as u ")
stmt.WriteString("ON u.id = ui.created_by ")
stmt.WriteString("ORDER BY ui.username ASC ")
rows, err := rdb.SQLConn.Query(stmt.String(), args...)
rows, err := rdb.SQLConn.Query("SELECT ui.id, ui.username, ui.email, ui.display_name, ui.created_at, ui.is_admin," +
" u.id, u.username, u.email, u.display_name FROM user_invite as ui INNER JOIN user as u ON u.id = ui.created_by ORDER BY ui.username ASC")
if err != nil {
return nil, err
}
defer rows.Close()
invites := []UserInvite{}
var ui UserInvite
var u User
defer func() {
if err := rows.Close(); err != nil {
logger.Log.Error(err)
}
}()
for rows.Next() {
if err = rows.Scan(&ui.ID, &ui.Username, &ui.Email, &ui.DisplayName, &ui.CreatedAt, &ui.IsAdmin, &u.ID, &u.Username, &u.Email, &u.DisplayName); err != nil {
@ -50,7 +44,7 @@ func (rdb SQLiteUserInviteDatasource) List() ([]UserInvite, error) {
return invites, nil
}
func (rdb SQLiteUserInviteDatasource) Get(inviteID int) (*UserInvite, error) {
func (rdb *SQLiteUserInviteDatasource) Get(inviteID int) (*UserInvite, error) {
var u User
var ui UserInvite
@ -69,7 +63,7 @@ func (rdb SQLiteUserInviteDatasource) Get(inviteID int) (*UserInvite, error) {
return &ui, nil
}
func (rdb SQLiteUserInviteDatasource) GetByHash(hash string) (*UserInvite, error) {
func (rdb *SQLiteUserInviteDatasource) GetByHash(hash string) (*UserInvite, error) {
var ui UserInvite
var u User
@ -88,7 +82,7 @@ func (rdb SQLiteUserInviteDatasource) GetByHash(hash string) (*UserInvite, error
return &ui, nil
}
func (rdb SQLiteUserInviteDatasource) Update(ui *UserInvite) error {
func (rdb *SQLiteUserInviteDatasource) Update(ui *UserInvite) error {
if _, err := rdb.SQLConn.Exec("UPDATE user_invite SET hash=?, username=?, email=?, display_name=?, is_admin=?, created_at=?, created_by=? "+
"WHERE id=? ", ui.Hash, ui.Username, ui.Email, ui.DisplayName, ui.IsAdmin, ui.CreatedBy.ID, ui.ID); err != nil {
return err
@ -97,8 +91,8 @@ func (rdb SQLiteUserInviteDatasource) Update(ui *UserInvite) error {
return nil
}
//Create creates an new user invitation
func (rdb SQLiteUserInviteDatasource) Create(ui *UserInvite) (int, error) {
// Create creates an new user invitation
func (rdb *SQLiteUserInviteDatasource) Create(ui *UserInvite) (int, error) {
res, err := rdb.SQLConn.Exec("INSERT INTO user_invite (hash, username, email, display_name, is_admin, created_at, created_by) VALUES(?, ?, ?, ?, ?, ?, ?);",
ui.Hash, ui.Username, ui.Email, ui.DisplayName, ui.IsAdmin, time.Now(), ui.CreatedBy.ID)
@ -111,11 +105,12 @@ func (rdb SQLiteUserInviteDatasource) Create(ui *UserInvite) (int, error) {
if err != nil {
return -1, err
}
return int(i), nil
}
//Count retuns the amount of users invitations
func (rdb SQLiteUserInviteDatasource) Count() (int, error) {
// Count retuns the amount of users invitations
func (rdb *SQLiteUserInviteDatasource) Count() (int, error) {
var total int
if err := rdb.SQLConn.QueryRow("SELECT count(id) FROM user_invite").Scan(&total); err != nil {
@ -125,13 +120,9 @@ func (rdb SQLiteUserInviteDatasource) Count() (int, error) {
return total, nil
}
//Removes an user invitation
func (rdb SQLiteUserInviteDatasource) Remove(inviteID int) error {
var stmt bytes.Buffer
stmt.WriteString("DELETE FROM user_invite WHERE id=?")
if _, err := rdb.SQLConn.Exec(stmt.String(), inviteID); err != nil {
// Remove removes an user invitation
func (rdb *SQLiteUserInviteDatasource) Remove(inviteID int) error {
if _, err := rdb.SQLConn.Exec("DELETE FROM user_invite WHERE id=?", inviteID); err != nil {
return err
}

View File

@ -1,20 +1,23 @@
package models
import (
"bytes"
"database/sql"
"git.hoogi.eu/snafu/go-blog/logger"
"strings"
"time"
)
//SQLiteUserDatasource providing an implementation of UserDatasourceService using SQLite
// SQLiteUserDatasource providing an implementation of UserDatasourceService using SQLite
type SQLiteUserDatasource struct {
SQLConn *sql.DB
}
//List returns a list of user
func (rdb SQLiteUserDatasource) List(p *Pagination) ([]User, error) {
var stmt bytes.Buffer
// List returns a list of users
func (rdb *SQLiteUserDatasource) List(p *Pagination) ([]User, error) {
var stmt strings.Builder
var args []interface{}
var users []User
var u User
stmt.WriteString("SELECT id, username, email, display_name, last_modified, active, is_admin FROM user ORDER BY username ASC ")
@ -29,11 +32,11 @@ func (rdb SQLiteUserDatasource) List(p *Pagination) ([]User, error) {
return nil, err
}
defer rows.Close()
users := []User{}
var u User
defer func() {
if err := rows.Close(); err != nil {
logger.Log.Error(err)
}
}()
for rows.Next() {
if err = rows.Scan(&u.ID, &u.Username, &u.Email, &u.DisplayName, &u.LastModified, &u.Active, &u.IsAdmin); err != nil {
@ -50,8 +53,8 @@ func (rdb SQLiteUserDatasource) List(p *Pagination) ([]User, error) {
return users, nil
}
//Get gets an user by his userID
func (rdb SQLiteUserDatasource) Get(userID int) (*User, error) {
// Get gets a user by his userID
func (rdb *SQLiteUserDatasource) Get(userID int) (*User, error) {
var u User
if err := rdb.SQLConn.QueryRow("SELECT u.id, u.username, u.email, u.display_name, u.last_modified, u.active, u.is_admin, u.salt "+
@ -64,8 +67,8 @@ func (rdb SQLiteUserDatasource) Get(userID int) (*User, error) {
return &u, nil
}
//GetByMail gets an user by his mail, includes the password and salt
func (rdb SQLiteUserDatasource) GetByMail(mail string) (*User, error) {
// GetByMail gets a user by his mail, includes the password and salt
func (rdb *SQLiteUserDatasource) GetByMail(mail string) (*User, error) {
var u User
if err := rdb.SQLConn.QueryRow("SELECT id, is_admin, active, display_name, username, email, salt, password FROM user WHERE email=? ", mail).
@ -75,8 +78,8 @@ func (rdb SQLiteUserDatasource) GetByMail(mail string) (*User, error) {
return &u, nil
}
//GetByUsername gets an user by his username, includes the password and salt
func (rdb SQLiteUserDatasource) GetByUsername(username string) (*User, error) {
// GetByUsername gets a user by his username, includes the password and salt
func (rdb *SQLiteUserDatasource) GetByUsername(username string) (*User, error) {
var u User
if err := rdb.SQLConn.QueryRow("SELECT id, is_admin, active, display_name, username, email, salt, password FROM user WHERE username=? ", username).
@ -86,8 +89,8 @@ func (rdb SQLiteUserDatasource) GetByUsername(username string) (*User, error) {
return &u, nil
}
//Create creates an new user
func (rdb SQLiteUserDatasource) Create(u *User) (int, error) {
// Create creates a new user
func (rdb *SQLiteUserDatasource) Create(u *User) (int, error) {
res, err := rdb.SQLConn.Exec("INSERT INTO user (salt, password, username, email, display_name, last_modified, active, is_admin) VALUES(?, ?, ?, ?, ?, ?, ?, ?);",
u.Salt, u.Password, u.Username, u.Email, u.DisplayName, time.Now(), u.Active, u.IsAdmin)
@ -100,13 +103,13 @@ func (rdb SQLiteUserDatasource) Create(u *User) (int, error) {
if err != nil {
return -1, err
}
return int(i), nil
}
//Update updates an user
func (rdb SQLiteUserDatasource) Update(u *User, changePassword bool) error {
var stmt bytes.Buffer
// Update updates an user
func (rdb *SQLiteUserDatasource) Update(u *User, changePassword bool) error {
var stmt strings.Builder
var args []interface{}
stmt.WriteString("UPDATE user SET display_name=?, username=?, email=?, last_modified=?, active=?, is_admin=? ")
@ -114,9 +117,9 @@ func (rdb SQLiteUserDatasource) Update(u *User, changePassword bool) error {
if changePassword {
stmt.WriteString(", salt=?, password=? ")
args = append(args, u.Salt, u.Password)
}
stmt.WriteString("WHERE id=?;")
args = append(args, u.ID)
@ -128,10 +131,9 @@ func (rdb SQLiteUserDatasource) Update(u *User, changePassword bool) error {
return nil
}
//Count retuns the amount of users matches the AdminCriteria
func (rdb SQLiteUserDatasource) Count(ac AdminCriteria) (int, error) {
var stmt bytes.Buffer
// Count returns the amount of users matches the AdminCriteria
func (rdb *SQLiteUserDatasource) Count(ac AdminCriteria) (int, error) {
var stmt strings.Builder
stmt.WriteString("SELECT count(id) FROM user ")
if ac == OnlyAdmins {
@ -149,13 +151,9 @@ func (rdb SQLiteUserDatasource) Count(ac AdminCriteria) (int, error) {
return total, nil
}
//Removes an user
func (rdb SQLiteUserDatasource) Remove(userID int) error {
var stmt bytes.Buffer
stmt.WriteString("DELETE FROM user WHERE id=?")
if _, err := rdb.SQLConn.Exec(stmt.String(), userID); err != nil {
// Remove removes an user
func (rdb *SQLiteUserDatasource) Remove(userID int) error {
if _, err := rdb.SQLConn.Exec("DELETE FROM user WHERE id=?", userID); err != nil {
return err
}

View File

@ -8,7 +8,7 @@ import (
"net/http"
"os"
c "git.hoogi.eu/snafu/go-blog/controllers"
"git.hoogi.eu/snafu/go-blog/handler"
m "git.hoogi.eu/snafu/go-blog/middleware"
"git.hoogi.eu/snafu/go-blog/settings"
@ -18,13 +18,13 @@ import (
"github.com/justinas/alice"
)
//InitRoutes initializes restricted and public routes
// InitRoutes initializes restricted and public routes
func InitRoutes(ctx *m.AppContext, cfg *settings.Settings) *mux.Router {
router := mux.NewRouter()
router = router.StrictSlash(false)
sr := router.PathPrefix("/").Subrouter()
csrf :=
rf :=
csrf.Protect([]byte(cfg.CSRF.RandomKey),
csrf.Secure(cfg.CSRF.CookieSecure),
csrf.FieldName(cfg.CSRF.CookieName),
@ -45,7 +45,7 @@ func InitRoutes(ctx *m.AppContext, cfg *settings.Settings) *mux.Router {
ar := router.PathPrefix("/admin").Subrouter()
restrictedChain := chain.Append(csrf).Append(ctx.AuthHandler)
restrictedChain := chain.Append(rf).Append(ctx.AuthHandler)
restrictedRoutes(ctx, ar, restrictedChain)
@ -89,109 +89,109 @@ func fileLoggingHandler(accessLogPath string) (flh func(http.Handler) http.Handl
}
func restrictedRoutes(ctx *m.AppContext, router *mux.Router, chain alice.Chain) {
//article
router.Handle("/articles", chain.Then(useTemplateHandler(ctx, c.AdminListArticlesHandler))).Methods("GET")
router.Handle("/articles/page/{page}", chain.Then(useTemplateHandler(ctx, c.AdminListArticlesHandler))).Methods("GET")
router.Handle("/article/new", chain.Then(useTemplateHandler(ctx, c.AdminArticleNewHandler))).Methods("GET")
router.Handle("/article/new", chain.Then(useTemplateHandler(ctx, c.AdminArticleNewPostHandler))).Methods("POST")
router.Handle("/article/edit/{articleID}", chain.Then(useTemplateHandler(ctx, c.AdminArticleEditHandler))).Methods("GET")
router.Handle("/article/edit/{articleID}", chain.Then(useTemplateHandler(ctx, c.AdminArticleEditPostHandler))).Methods("POST")
router.Handle("/article/publish/{articleID}", chain.Then(useTemplateHandler(ctx, c.AdminArticlePublishHandler))).Methods("GET")
router.Handle("/article/publish/{articleID}", chain.Then(useTemplateHandler(ctx, c.AdminArticlePublishPostHandler))).Methods("POST")
router.Handle("/article/delete/{articleID}", chain.Then(useTemplateHandler(ctx, c.AdminArticleDeleteHandler))).Methods("GET")
router.Handle("/article/delete/{articleID}", chain.Then(useTemplateHandler(ctx, c.AdminArticleDeletePostHandler))).Methods("POST")
router.Handle("/article/{articleID}", chain.Then(useTemplateHandler(ctx, c.AdminPreviewArticleByIDHandler))).Methods("GET")
// article
router.Handle("/articles", chain.Then(useTemplateHandler(ctx, handler.AdminListArticlesHandler))).Methods("GET")
router.Handle("/articles/page/{page}", chain.Then(useTemplateHandler(ctx, handler.AdminListArticlesHandler))).Methods("GET")
router.Handle("/article/new", chain.Then(useTemplateHandler(ctx, handler.AdminArticleNewHandler))).Methods("GET")
router.Handle("/article/new", chain.Then(useTemplateHandler(ctx, handler.AdminArticleNewPostHandler))).Methods("POST")
router.Handle("/article/edit/{articleID}", chain.Then(useTemplateHandler(ctx, handler.AdminArticleEditHandler))).Methods("GET")
router.Handle("/article/edit/{articleID}", chain.Then(useTemplateHandler(ctx, handler.AdminArticleEditPostHandler))).Methods("POST")
router.Handle("/article/publish/{articleID}", chain.Then(useTemplateHandler(ctx, handler.AdminArticlePublishHandler))).Methods("GET")
router.Handle("/article/publish/{articleID}", chain.Then(useTemplateHandler(ctx, handler.AdminArticlePublishPostHandler))).Methods("POST")
router.Handle("/article/delete/{articleID}", chain.Then(useTemplateHandler(ctx, handler.AdminArticleDeleteHandler))).Methods("GET")
router.Handle("/article/delete/{articleID}", chain.Then(useTemplateHandler(ctx, handler.AdminArticleDeletePostHandler))).Methods("POST")
router.Handle("/article/{articleID}", chain.Then(useTemplateHandler(ctx, handler.AdminPreviewArticleByIDHandler))).Methods("GET")
//user
router.Handle("/user/profile", chain.Then(useTemplateHandler(ctx, c.AdminProfileHandler))).Methods("GET")
router.Handle("/user/profile", chain.Then(useTemplateHandler(ctx, c.AdminProfilePostHandler))).Methods("POST")
router.Handle("/users", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUsersHandler))).Methods("GET")
router.Handle("/users/page/{page}", chain.Then(useTemplateHandler(ctx, c.AdminUsersHandler))).Methods("GET")
router.Handle("/user/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserNewHandler))).Methods("GET")
router.Handle("/user/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserNewPostHandler))).Methods("POST")
router.Handle("/user/edit/{userID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserEditHandler))).Methods("GET")
router.Handle("/user/edit/{userID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserEditPostHandler))).Methods("POST")
router.Handle("/user/delete/{userID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserDeleteHandler))).Methods("GET")
router.Handle("/user/delete/{userID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserDeletePostHandler))).Methods("POST")
// user
router.Handle("/user/profile", chain.Then(useTemplateHandler(ctx, handler.AdminProfileHandler))).Methods("GET")
router.Handle("/user/profile", chain.Then(useTemplateHandler(ctx, handler.AdminProfilePostHandler))).Methods("POST")
router.Handle("/users", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUsersHandler))).Methods("GET")
router.Handle("/users/page/{page}", chain.Then(useTemplateHandler(ctx, handler.AdminUsersHandler))).Methods("GET")
router.Handle("/user/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserNewHandler))).Methods("GET")
router.Handle("/user/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserNewPostHandler))).Methods("POST")
router.Handle("/user/edit/{userID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserEditHandler))).Methods("GET")
router.Handle("/user/edit/{userID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserEditPostHandler))).Methods("POST")
router.Handle("/user/delete/{userID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserDeleteHandler))).Methods("GET")
router.Handle("/user/delete/{userID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserDeletePostHandler))).Methods("POST")
//user invites
router.Handle("/user-invite/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserInviteNewHandler))).Methods("GET")
router.Handle("/user-invite/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserInviteNewPostHandler))).Methods("POST")
router.Handle("/user-invite/resend/{inviteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserInviteResendPostHandler))).Methods("POST")
router.Handle("/user-invite/delete/{inviteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserInviteDeleteHandler))).Methods("GET")
router.Handle("/user-invite/delete/{inviteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminUserInviteDeletePostHandler))).Methods("POST")
// user invites
router.Handle("/user-invite/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserInviteNewHandler))).Methods("GET")
router.Handle("/user-invite/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserInviteNewPostHandler))).Methods("POST")
router.Handle("/user-invite/resend/{inviteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserInviteResendPostHandler))).Methods("POST")
router.Handle("/user-invite/delete/{inviteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserInviteDeleteHandler))).Methods("GET")
router.Handle("/user-invite/delete/{inviteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminUserInviteDeletePostHandler))).Methods("POST")
//site
router.Handle("/sites", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminSitesHandler))).Methods("GET")
router.Handle("/site/page/{page}", chain.Then(useTemplateHandler(ctx, c.AdminSitesHandler))).Methods("GET")
router.Handle("/site/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminSiteNewHandler))).Methods("GET")
router.Handle("/site/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminSiteNewPostHandler))).Methods("POST")
router.Handle("/site/publish/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminSitePublishHandler))).Methods("GET")
router.Handle("/site/publish/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminSitePublishPostHandler))).Methods("POST")
router.Handle("/site/edit/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminSiteEditHandler))).Methods("GET")
router.Handle("/site/edit/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminSiteEditPostHandler))).Methods("POST")
router.Handle("/site/delete/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminSiteDeleteHandler))).Methods("GET")
router.Handle("/site/delete/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminSiteDeletePostHandler))).Methods("POST")
router.Handle("/site/order/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, c.AdminSiteOrderHandler))).Methods("POST")
router.Handle("/site/{siteID:[0-9]+}}", chain.Then(useTemplateHandler(ctx, c.AdminGetSiteHandler))).Methods("GET")
// site
router.Handle("/sites", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminSitesHandler))).Methods("GET")
router.Handle("/site/page/{page}", chain.Then(useTemplateHandler(ctx, handler.AdminSitesHandler))).Methods("GET")
router.Handle("/site/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminSiteNewHandler))).Methods("GET")
router.Handle("/site/new", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminSiteNewPostHandler))).Methods("POST")
router.Handle("/site/publish/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminSitePublishHandler))).Methods("GET")
router.Handle("/site/publish/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminSitePublishPostHandler))).Methods("POST")
router.Handle("/site/edit/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminSiteEditHandler))).Methods("GET")
router.Handle("/site/edit/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminSiteEditPostHandler))).Methods("POST")
router.Handle("/site/delete/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminSiteDeleteHandler))).Methods("GET")
router.Handle("/site/delete/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminSiteDeletePostHandler))).Methods("POST")
router.Handle("/site/order/{siteID}", chain.Append(ctx.RequireAdmin).Then(useTemplateHandler(ctx, handler.AdminSiteOrderHandler))).Methods("POST")
router.Handle("/site/{siteID:[0-9]+}}", chain.Then(useTemplateHandler(ctx, handler.AdminGetSiteHandler))).Methods("GET")
//article
router.Handle("/categories", chain.Then(useTemplateHandler(ctx, c.AdminListCategoriesHandler))).Methods("GET")
router.Handle("/category/{categoryID:[0-9]+}}", chain.Then(useTemplateHandler(ctx, c.AdminGetCategoryHandler))).Methods("POST")
router.Handle("/category/new", chain.Then(useTemplateHandler(ctx, c.AdminCategoryNewHandler))).Methods("GET")
router.Handle("/category/new", chain.Then(useTemplateHandler(ctx, c.AdminCategoryNewPostHandler))).Methods("POST")
router.Handle("/category/edit/{categoryID}", chain.Then(useTemplateHandler(ctx, c.AdminCategoryEditHandler))).Methods("GET")
router.Handle("/category/edit/{categoryID}", chain.Then(useTemplateHandler(ctx, c.AdminCategoryEditPostHandler))).Methods("POST")
router.Handle("/category/delete/{categoryID}", chain.Then(useTemplateHandler(ctx, c.AdminCategoryDeleteHandler))).Methods("GET")
router.Handle("/category/delete/{categoryID}", chain.Then(useTemplateHandler(ctx, c.AdminCategoryDeletePostHandler))).Methods("POST")
// article
router.Handle("/categories", chain.Then(useTemplateHandler(ctx, handler.AdminListCategoriesHandler))).Methods("GET")
router.Handle("/category/{categoryID:[0-9]+}}", chain.Then(useTemplateHandler(ctx, handler.AdminGetCategoryHandler))).Methods("POST")
router.Handle("/category/new", chain.Then(useTemplateHandler(ctx, handler.AdminCategoryNewHandler))).Methods("GET")
router.Handle("/category/new", chain.Then(useTemplateHandler(ctx, handler.AdminCategoryNewPostHandler))).Methods("POST")
router.Handle("/category/edit/{categoryID}", chain.Then(useTemplateHandler(ctx, handler.AdminCategoryEditHandler))).Methods("GET")
router.Handle("/category/edit/{categoryID}", chain.Then(useTemplateHandler(ctx, handler.AdminCategoryEditPostHandler))).Methods("POST")
router.Handle("/category/delete/{categoryID}", chain.Then(useTemplateHandler(ctx, handler.AdminCategoryDeleteHandler))).Methods("GET")
router.Handle("/category/delete/{categoryID}", chain.Then(useTemplateHandler(ctx, handler.AdminCategoryDeletePostHandler))).Methods("POST")
//file
router.Handle("/files", chain.Then(useTemplateHandler(ctx, c.AdminListFilesHandler))).Methods("GET")
router.Handle("/files/page/{page}", chain.Then(useTemplateHandler(ctx, c.AdminListFilesHandler))).Methods("GET")
router.Handle("/file/upload", chain.Then(useTemplateHandler(ctx, c.AdminUploadFileHandler))).Methods("GET")
router.Handle("/file/upload", chain.Then(useTemplateHandler(ctx, c.AdminUploadFilePostHandler))).Methods("POST")
router.Handle("/file/toggleInline/{fileID}", chain.Then(useTemplateHandler(ctx, c.AdminToggleInlineFilePostHandler))).Methods("POST")
router.Handle("/file/delete/{fileID}", chain.Then(useTemplateHandler(ctx, c.AdminUploadDeleteHandler))).Methods("GET")
router.Handle("/file/delete/{fileID}", chain.Then(useTemplateHandler(ctx, c.AdminUploadDeletePostHandler))).Methods("POST")
// file
router.Handle("/files", chain.Then(useTemplateHandler(ctx, handler.AdminListFilesHandler))).Methods("GET")
router.Handle("/files/page/{page}", chain.Then(useTemplateHandler(ctx, handler.AdminListFilesHandler))).Methods("GET")
router.Handle("/file/upload", chain.Then(useTemplateHandler(ctx, handler.AdminUploadFileHandler))).Methods("GET")
router.Handle("/file/upload", chain.Then(useTemplateHandler(ctx, handler.AdminUploadFilePostHandler))).Methods("POST")
router.Handle("/file/toggleInline/{fileID}", chain.Then(useTemplateHandler(ctx, handler.AdminToggleInlineFilePostHandler))).Methods("POST")
router.Handle("/file/delete/{fileID}", chain.Then(useTemplateHandler(ctx, handler.AdminUploadDeleteHandler))).Methods("GET")
router.Handle("/file/delete/{fileID}", chain.Then(useTemplateHandler(ctx, handler.AdminUploadDeletePostHandler))).Methods("POST")
router.Handle("/logout", chain.Then(useTemplateHandler(ctx, c.LogoutHandler))).Methods("GET")
router.Handle("/logout", chain.Then(useTemplateHandler(ctx, handler.LogoutHandler))).Methods("GET")
router.Handle("/json/session/keep-alive", chain.Then(useJSONHandler(ctx, c.KeepAliveSessionHandler))).Methods("POST")
router.Handle("/json/file/upload", chain.Then(useJSONHandler(ctx, c.AdminUploadJSONFilePostHandler))).Methods("POST")
router.Handle("/json/session/keep-alive", chain.Then(useJSONHandler(ctx, handler.KeepAliveSessionHandler))).Methods("POST")
router.Handle("/json/file/upload", chain.Then(useJSONHandler(ctx, handler.AdminUploadJSONFilePostHandler))).Methods("POST")
}
func publicRoutes(ctx *m.AppContext, router *mux.Router, chain alice.Chain) {
fh := c.FileHandler{
fh := handler.FileHandler{
Context: ctx,
}
router.Handle("/", chain.Then(useTemplateHandler(ctx, c.ListArticlesHandler))).Methods("GET")
router.Handle("/articles/category/{categorySlug}", chain.Then(useTemplateHandler(ctx, c.ListArticlesCategoryHandler))).Methods("GET")
router.Handle("/articles/category/{categorySlug}/{page}", chain.Then(useTemplateHandler(ctx, c.ListArticlesCategoryHandler))).Methods("GET")
router.Handle("/index", chain.Then(useTemplateHandler(ctx, c.IndexArticlesHandler))).Methods("GET")
router.Handle("/index/category/{categorySlug}", chain.Then(useTemplateHandler(ctx, c.IndexArticlesCategoryHandler))).Methods("GET")
router.Handle("/", chain.Then(useTemplateHandler(ctx, handler.ListArticlesHandler))).Methods("GET")
router.Handle("/articles/category/{categorySlug}", chain.Then(useTemplateHandler(ctx, handler.ListArticlesCategoryHandler))).Methods("GET")
router.Handle("/articles/category/{categorySlug}/{page}", chain.Then(useTemplateHandler(ctx, handler.ListArticlesCategoryHandler))).Methods("GET")
router.Handle("/index", chain.Then(useTemplateHandler(ctx, handler.IndexArticlesHandler))).Methods("GET")
router.Handle("/index/category/{categorySlug}", chain.Then(useTemplateHandler(ctx, handler.IndexArticlesCategoryHandler))).Methods("GET")
router.Handle("/articles/page/{page}", chain.Then(useTemplateHandler(ctx, c.ListArticlesHandler))).Methods("GET")
router.Handle("/article/{year}/{month}/{slug}", chain.Then(useTemplateHandler(ctx, c.GetArticleHandler))).Methods("GET")
router.Handle("/article/by-id/{articleID}", chain.Then(useTemplateHandler(ctx, c.GetArticleByIDHandler))).Methods("GET")
router.Handle("/articles/page/{page}", chain.Then(useTemplateHandler(ctx, handler.ListArticlesHandler))).Methods("GET")
router.Handle("/article/{year}/{month}/{slug}", chain.Then(useTemplateHandler(ctx, handler.GetArticleHandler))).Methods("GET")
router.Handle("/article/by-id/{articleID}", chain.Then(useTemplateHandler(ctx, handler.GetArticleByIDHandler))).Methods("GET")
router.Handle("/rss.xml", chain.Then(useXMLHandler(ctx, c.RSSFeed))).Methods("GET")
router.Handle("/rss.xml", chain.Then(useXMLHandler(ctx, handler.RSSFeed))).Methods("GET")
router.Handle("/site/{site}", chain.Then(useTemplateHandler(ctx, c.GetSiteHandler))).Methods("GET")
router.Handle("/site/{site}", chain.Then(useTemplateHandler(ctx, handler.GetSiteHandler))).Methods("GET")
router.Handle("/file/{uniquename}", chain.ThenFunc(fh.FileGetHandler)).Methods("GET")
router.Handle("/admin", chain.Then(useTemplateHandler(ctx, c.LoginHandler))).Methods("GET")
router.Handle("/admin", chain.Then(useTemplateHandler(ctx, c.LoginPostHandler))).Methods("POST")
router.Handle("/admin", chain.Then(useTemplateHandler(ctx, handler.LoginHandler))).Methods("GET")
router.Handle("/admin", chain.Then(useTemplateHandler(ctx, handler.LoginPostHandler))).Methods("POST")
router.Handle("/admin/forgot-password", chain.Then(useTemplateHandler(ctx, c.ForgotPasswordHandler))).Methods("GET")
router.Handle("/admin/forgot-password", chain.Then(useTemplateHandler(ctx, c.ForgotPasswordPostHandler))).Methods("POST")
router.Handle("/admin/forgot-password", chain.Then(useTemplateHandler(ctx, handler.ForgotPasswordHandler))).Methods("GET")
router.Handle("/admin/forgot-password", chain.Then(useTemplateHandler(ctx, handler.ForgotPasswordPostHandler))).Methods("POST")
router.Handle("/admin/reset-password/{hash}", chain.Then(useTemplateHandler(ctx, c.ResetPasswordHandler))).Methods("GET")
router.Handle("/admin/reset-password/{hash}", chain.Then(useTemplateHandler(ctx, c.ResetPasswordPostHandler))).Methods("POST")
router.Handle("/admin/reset-password/{hash}", chain.Then(useTemplateHandler(ctx, handler.ResetPasswordHandler))).Methods("GET")
router.Handle("/admin/reset-password/{hash}", chain.Then(useTemplateHandler(ctx, handler.ResetPasswordPostHandler))).Methods("POST")
router.Handle("/admin/activate-account/{hash}", chain.Then(useTemplateHandler(ctx, c.ActivateAccountHandler))).Methods("GET")
router.Handle("/admin/activate-account/{hash}", chain.Then(useTemplateHandler(ctx, c.ActivateAccountPostHandler))).Methods("POST")
router.Handle("/admin/activate-account/{hash}", chain.Then(useTemplateHandler(ctx, handler.ActivateAccountHandler))).Methods("GET")
router.Handle("/admin/activate-account/{hash}", chain.Then(useTemplateHandler(ctx, handler.ActivateAccountPostHandler))).Methods("POST")
}
func useTemplateHandler(ctx *m.AppContext, handler m.Handler) m.TemplateHandler {

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
//Package cfg parses and validates the configuration
// Package settings parses and validates the configuration
package settings
import (
@ -15,8 +15,8 @@ import (
"time"
"git.hoogi.eu/snafu/cfg"
"git.hoogi.eu/snafu/go-blog/components/logger"
"git.hoogi.eu/snafu/go-blog/utils"
"git.hoogi.eu/snafu/go-blog/crypt"
"git.hoogi.eu/snafu/go-blog/logger"
)
type LoginMethod int
@ -55,7 +55,7 @@ func (afe *AllowedFileExts) Unmarshal(value string) error {
kv[trimmed] = trimmed
*afe = AllowedFileExts(kv)
*afe = kv
}
return nil
@ -183,7 +183,7 @@ func LoadConfig(filename string) (*Settings, error) {
}
func (cfg *Settings) CheckConfig() error {
//check log file is rw in production mode
// check log file is rw in production mode
if cfg.Environment != "dev" {
if _, err := os.OpenFile(cfg.Log.File, os.O_RDONLY|os.O_CREATE, 0640); err != nil {
return fmt.Errorf("config 'log_file': could not open log file %s error %v", cfg.Log.File, err)
@ -258,7 +258,7 @@ func (cfg *Settings) CheckConfig() error {
cfg.Application.Favicon = "assets/favicon.ico"
}
//server settings
// server settings
if cfg.Server.UseTLS {
if _, err := os.Open(cfg.Server.Cert); err != nil {
return fmt.Errorf("config: could not open certificate %s error %v", cfg.Server.Cert, err)
@ -285,16 +285,10 @@ func (cfg *Settings) GenerateCSRF() (bool, error) {
var b []byte
if _, err := os.Stat(csrfTokenFilename); os.IsNotExist(err) {
//create a random csrf token
r := utils.RandomSource{
CharsToGen: utils.AlphaUpperLowerNumericSpecial,
}
// create a random csrf token
b = crypt.AlphaUpperLowerNumericSpecial.RandomSequence(32)
b = r.RandomSequence(32)
err := ioutil.WriteFile(csrfTokenFilename, b, 0640)
if err != nil {
if err := ioutil.WriteFile(csrfTokenFilename, b, 0640); err != nil {
return false, err
}
@ -302,7 +296,7 @@ func (cfg *Settings) GenerateCSRF() (bool, error) {
return true, nil
} else {
//read existing csrf token
// read existing csrf token
b, err = ioutil.ReadFile(csrfTokenFilename)
if err != nil {

36
slug/slug.go Normal file
View File

@ -0,0 +1,36 @@
package slug
import (
"regexp"
"strconv"
"strings"
"unicode"
)
var multipleDashes = regexp.MustCompile(`[-]{2,}`)
// CreateURLSafeSlug creates a URL safe slug to use in URL
func CreateURLSafeSlug(input string, suffix int) string {
input = strings.Replace(input, "-", "", -1)
input = strings.Map(func(r rune) rune {
switch {
case r == ' ':
return '-'
case unicode.IsLetter(r), unicode.IsDigit(r):
return r
default:
return -1
}
}, strings.ToLower(strings.TrimSpace(input)))
input = strings.Trim(input, "-")
input = multipleDashes.ReplaceAllString(input, "-")
if suffix > 0 {
input += strconv.Itoa(suffix)
}
return input
}

View File

@ -2,12 +2,12 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package utils_test
package slug_test
import (
"testing"
"git.hoogi.eu/snafu/go-blog/utils"
"git.hoogi.eu/snafu/go-blog/slug"
)
func TestCreateURLSafeSlug(t *testing.T) {
@ -26,7 +26,7 @@ func TestCreateURLSafeSlug(t *testing.T) {
}
for i := 0; i < len(testcases)-1; i = i + 2 {
actual := utils.CreateURLSafeSlug(testcases[i], 0)
actual := slug.CreateURLSafeSlug(testcases[i], 0)
if actual != testcases[i+1] {
t.Errorf("Got: '%s'; want '%s'", actual, testcases[i+1])

View File

@ -10,7 +10,7 @@
<form id="autosave-form" action="/admin/article/new" method="post">
<label for="category">Category</label>
<select name="categoryID">
<select id="category" name="categoryID">
<option></option>
{{range .categories}}
<option value="{{.ID}}">{{.Name}}</option>

View File

@ -10,7 +10,8 @@
{{with .article}}
<form id="autosave-form" action="/admin/article/edit/{{.ID}}" method="post">
<select name="categoryID">
<label for="category">Category</label>
<select id="category" name="categoryID">
<option></option>
{{range $.categories}}
<option{{if $.article.CID.Valid}}{{if eq $.article.CID.Int64 .ID}} selected="selected"{{end}}{{end}} value="{{.ID}}">{{.Name}}</option>

View File

@ -5,7 +5,7 @@
</footer>
<script type="text/javascript">
var directUpload = document.getElementById('direct-upload');
let directUpload = document.getElementById('direct-upload');
directUpload && directUpload.addEventListener("submit", function(e) {
e.preventDefault();
@ -41,7 +41,7 @@
table.style.display = "table";
let tableBody = table.tBodies[0]
let tableBody = table.tBodies[0];
let row = tableBody.insertRow(-1);
@ -70,11 +70,11 @@
});
});
var persistForm = function() {
let persistForm = function() {
let form = document.getElementById("autosave-form");
if(form === undefined) {
return
return;
}
let data = {};
@ -99,7 +99,7 @@
localStorage.setItem('AUTOSAVE'+action.split('/').join('_'), JSON.stringify(data));
}
var autoSaveForm = document.getElementById('autosave-form');
let autoSaveForm = document.getElementById('autosave-form');
autoSaveForm && autoSaveForm.addEventListener("submit", function(e) {
let form = document.getElementById("autosave-form");
@ -107,11 +107,11 @@
localStorage.removeItem('AUTOSAVE'+action.split('/').join('_'));
});
var loadForm = function() {
let loadForm = function() {
let form = document.getElementById("autosave-form");
if(form === undefined) {
return
return;
}
let action = form.getAttribute("action");
@ -119,23 +119,25 @@
let json = localStorage.getItem('AUTOSAVE'+action.split('/').join('_'));
let obj = JSON.parse(json);
if (typeof obj === 'object' && obj !== null) {
if (Object.keys(obj).length > 0) {
for (let key in obj) {
if(key != "action") {
form.elements[key].value = obj[key]
}
}
if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
return;
}
let div = document.createElement('div');
let main = document.querySelector('main');
div.innerHTML = '<div style="margin-top: 10px" class="alert alert-info" role="status">Unsaved changes were loaded.</div>';
main.prepend(div);
}
}
if (Object.keys(obj).length > 0) {
for (let key in obj) {
if(key != "action") {
form.elements[key].value = obj[key];
}
}
let div = document.createElement('div');
let main = document.querySelector('main');
div.innerHTML = '<div style="margin-top: 10px" class="alert alert-info" role="status">Unsaved changes were loaded. <button>Reset</button></div>';
main.prepend(div);
}
}
var doKeepAliveRequest = function() {
let doKeepAliveRequest = function() {
fetch('/admin/json/session/keep-alive',
{
method: 'POST',
@ -144,8 +146,8 @@
},
})
};
var toggleContainer = function() {
let toggleContainer = function() {
let ahref = document.getElementById('toggleContainer');
let val = ahref.text
@ -164,8 +166,8 @@
};
let curPath = window.location.pathname;
let autoSaveInterval=5*1000;
let keepAliveInterval={{KeepAliveInterval}}*1000;
let autoSaveInterval = 5*1000;
let keepAliveInterval = {{KeepAliveInterval}}*1000;
if (curPath === "/admin/article/new" || curPath.includes("/admin/article/edit")
|| curPath === "/admin/site/new" || curPath.includes("/admin/site/edit")) {
@ -175,7 +177,6 @@
setInterval(persistForm, autoSaveInterval);
}
</script>
</div>
</body>
</html>
{{end}}

View File

@ -1,92 +0,0 @@
// Copyright 2018 Lars Hoogestraat
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package utils
import (
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"io"
"math/big"
"golang.org/x/crypto/bcrypt"
)
const (
//AlphaUpper all upper alphas chars
AlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
//AlphaLower all lowers alphas chars
AlphaLower = "abcdefghijklmnopqrstuvwxyz"
//AlphaUpperLower all upper and lowers aplhas chars
AlphaUpperLower = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
//AlphaUpperLowerNumeric all upper lowers alphas and numerics
AlphaUpperLowerNumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz"
//AlphaUpperLowerNumericSpecial all upper lowers alphas, numerics and special chas
AlphaUpperLowerNumericSpecial = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456890" +
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
)
//RandomSource express which chars should be considered
type RandomSource struct {
CharsToGen string
}
//RandomSequence returns random character with given length;
//random source express which chars should be considered
func (r RandomSource) RandomSequence(length int) []byte {
result := make([]byte, length)
for i := 0; i < length; i++ {
char, _ := rand.Int(rand.Reader, big.NewInt(int64(len(r.CharsToGen))))
result[i] = r.CharsToGen[int(char.Int64())]
}
return result
}
//RandomSecureKey returns random character with given length
func RandomSecureKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}
//CryptPassword bcrypts a password at given costs
func CryptPassword(password []byte, cost int) ([]byte, error) {
s, err := bcrypt.GenerateFromPassword(password, cost)
if err != nil {
return nil, err
}
return s, nil
}
//GenerateSalt generates a random salt with alphanumerics and some special characters
func GenerateSalt() []byte {
r := RandomSource{
CharsToGen: AlphaUpperLowerNumericSpecial,
}
return r.RandomSequence(32)
}
//EncodeBase64 encodes a string to base64
func EncodeBase64(input string) string {
return base64.StdEncoding.EncodeToString([]byte(input))
}
//DecodeBase64 descodes a string to base64
func DecodeBase64(b64 string) (string, error) {
out, err := base64.StdEncoding.DecodeString(b64)
return string(out), err
}
func RandomHash(length int) string {
hash := sha512.New()
hash.Write(RandomSecureKey(length))
return hex.EncodeToString(hash.Sum(nil))
}

View File

@ -1,136 +0,0 @@
// Copyright 2018 Lars Hoogestraat
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package utils
import (
"bytes"
"regexp"
"strconv"
"strings"
"unicode"
)
//AppendString uses byte buffer to append multiple strings
func AppendString(s ...string) string {
var buffer bytes.Buffer
for _, value := range s {
buffer.WriteString(value)
}
return buffer.String()
}
//AppendBytes uses byte buffer to append multiple byte arrays
func AppendBytes(s ...[]byte) []byte {
var buffer bytes.Buffer
for _, value := range s {
buffer.Write(value)
}
return buffer.Bytes()
}
//TrimmedStringIsEmpty trims spaces returns true if length is 0
func TrimmedStringIsEmpty(s string) bool {
return len(strings.TrimSpace(s)) == 0
}
//IsOneOfStringsEmpty checks if one of the given strings is empty
func IsOneOfStringsEmpty(s ...string) bool {
for _, value := range s {
if len(value) == 0 {
return true
}
}
return true
}
var filenameSubs = map[rune]string{
'/': "",
'\\': "",
':': "",
'*': "",
'?': "",
'"': "",
'<': "",
'>': "",
'|': "",
' ': "",
}
func isDot(r rune) bool {
return '.' == r
}
//SanitizeFilename sanitizes a filename for safe use when serving file
func SanitizeFilename(s string) string {
s = strings.TrimFunc(s, unicode.IsSpace)
s = removeControlCharacters(s)
s = substitute(s, filenameSubs)
s = strings.TrimLeftFunc(s, isDot)
return s
}
var slugSubs = map[rune]string{
'&': "",
'$': "",
'+': "",
',': "",
'/': "",
':': "",
';': "",
'=': "",
'?': "",
'@': "",
'#': "",
'!': "",
'\'': "",
'(': "",
')': "",
'*': "",
'%': "",
}
var multipleDashes = regexp.MustCompile(`[-]{2,}`)
//CreateURLSafeSlug creates a url safe slug to use in urls
func CreateURLSafeSlug(input string, suffix int) string {
input = removeControlCharacters(input)
input = substitute(input, slugSubs)
input = strings.TrimSpace(input)
input = strings.Replace(input, " ", "-", -1)
input = strings.ToLower(input)
input = multipleDashes.ReplaceAllString(input, "-")
if suffix > 0 {
input += strconv.Itoa(suffix)
}
return input
}
func substitute(input string, subs map[rune]string) string {
var b bytes.Buffer
for _, c := range input {
if _, ok := subs[c]; ok {
b.WriteString(subs[c])
} else {
b.WriteRune(c)
}
}
return b.String()
}
func removeControlCharacters(input string) string {
var b bytes.Buffer
for _, c := range input {
if c > 31 {
b.WriteRune(c)
}
}
return b.String()
}