initial commit

This commit is contained in:
konrad 2018-06-10 11:11:41 +02:00 committed by kolaente
commit 479cf54ada
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
595 changed files with 427508 additions and 0 deletions

3
.gitignore vendored Normal file

@ -0,0 +1,3 @@
.idea/
config.ini
*.db

165
LICENSE Normal file

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

BIN
Library Executable file

Binary file not shown.

156
Makefile Normal file

@ -0,0 +1,156 @@
DIST := dist
IMPORT := git.kolaente.de/konrad/list
SED_INPLACE := sed -i
ifeq ($(OS), Windows_NT)
EXECUTABLE := Library.exe
else
EXECUTABLE := Library
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
endif
endif
GOFILES := $(shell find . -name "*.go" -type f ! -path "./vendor/*" ! -path "*/bindata.go")
GOFMT ?= gofmt -s
GOFLAGS := -i -v
EXTRA_GOFLAGS ?=
LDFLAGS := -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.Tags=$(TAGS)"
PACKAGES ?= $(filter-out git.kolaente.de/konrad/list/integrations,$(shell go list ./... | grep -v /vendor/))
SOURCES ?= $(shell find . -name "*.go" -type f)
TAGS ?=
TMPDIR := $(shell mktemp -d 2>/dev/null || mktemp -d -t 'kasino-temp')
ifeq ($(OS), Windows_NT)
EXECUTABLE := Library.exe
else
EXECUTABLE := Library
endif
ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG))
else
ifneq ($(DRONE_BRANCH),)
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
else
VERSION ?= master
endif
endif
.PHONY: all
all: build
.PHONY: clean
clean:
go clean -i ./...
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA)
.PHONY: test
test:
go test -cover $(PACKAGES)
required-gofmt-version:
@go version | grep -q '\(1.7\|1.8\|1.9\|1.10\)' || { echo "We require go version 1.7, 1.8, 1.9 or 1.10 to format code" >&2 && exit 1; }
.PHONY: lint
lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/golang/lint/golint; \
fi
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
.PHONY: fmt
fmt: required-gofmt-version
$(GOFMT) -w $(GOFILES)
.PHONY: fmt-check
fmt-check: required-gofmt-version
# get all go files and run go fmt on them
@diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
echo "$${diff}"; \
exit 1; \
fi;
.PHONY: install
install: $(wildcard *.go)
go install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
.PHONY: build
build: $(EXECUTABLE)
$(EXECUTABLE): $(SOURCES)
go build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
.PHONY: release
release: release-dirs release-windows release-linux release-darwin release-frontend release-copy release-check release-os-package
.PHONY: release-dirs
release-dirs:
mkdir -p $(DIST)/binaries $(DIST)/release $(DIST)/zip
.PHONY: release-frontend
release-frontend:
mv frontend/siteconfig.json frontend/siteconfig.json.old; \
echo '{"API_URL": "/api/v1/"}' > frontend/siteconfig.json; \
npm --prefix frontend run build; \
rm frontend/dist/siteconfig.json; \
mv frontend/siteconfig.json.old frontend/siteconfig.json;
.PHONY: release-windows
release-windows:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/karalabe/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out Library-$(VERSION) .
ifeq ($(CI),drone)
mv /build/* $(DIST)/binaries
endif
.PHONY: release-linux
release-linux:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/karalabe/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/*' -out Library-$(VERSION) .
ifeq ($(CI),drone)
mv /build/* $(DIST)/binaries
endif
.PHONY: release-darwin
release-darwin:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/karalabe/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out Library-$(VERSION) .
ifeq ($(CI),drone)
mv /build/* $(DIST)/binaries
endif
.PHONY: release-copy
release-copy:
$(foreach file,$(wildcard $(DIST)/binaries/$(EXECUTABLE)-*),cp $(file) $(DIST)/release/$(notdir $(file));)
mkdir $(DIST)/release/frontend
cp frontend/dist $(DIST)/release/frontend/ -R
.PHONY: release-check
release-check:
cd $(DIST)/release; $(foreach file,$(wildcard $(DIST)/release/$(EXECUTABLE)-*),sha256sum $(notdir $(file)) > $(notdir $(file)).sha256;)
.PHONY: release-os-package
release-os-package:
$(foreach file,$(filter-out %.sha256,$(wildcard $(DIST)/release/$(EXECUTABLE)-*)),mkdir $(file)-full;mv $(file) $(file)-full/; mv $(file).sha256 $(file)-full/; cp config.ini.sample $(file)-full/config.ini; cp $(DIST)/release/frontend $(file)-full/ -R; cp LICENSE $(file)-full/; )
rm $(DIST)/release/frontend -rf
.PHONY: release-zip
release-zip:
$(foreach file,$(wildcard $(DIST)/release/$(EXECUTABLE)-*),cd $(file); zip -r ../../zip/$(shell basename $(file)).zip *; cd ../../../; )

14
config.ini.sample Normal file

@ -0,0 +1,14 @@
[General]
JWTSecret = blablaGEHEMIN§)!§
; The interface on which to run the webserver
Interface = :8080
[Database]
Type = mysql
User = root
Password = supersecret
Host = 127.0.0.1
Database = library
ShowQueries = false
; When using sqlite, this is the path where to store the data
; Path = ./library.db

63
main.go Normal file

@ -0,0 +1,63 @@
package main
import (
"git.kolaente.de/konrad/list/models"
"git.kolaente.de/konrad/list/routes"
"context"
"fmt"
"os"
"os/signal"
"time"
)
// UserLogin Object to recive user credentials in JSON format
type UserLogin struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
}
// Version sets the version to be printed to the user. Gets overwritten by "make release" or "make build" with last git commit or tag.
var Version = "1.0"
func main() {
// Init Config
err := models.SetConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Set Engine
err = models.SetEngine()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Version notification
fmt.Println("Library version", Version)
// Start the webserver
e := routes.NewEcho()
routes.RegisterRoutes(e)
// Start server
go func() {
if err := e.Start(models.Config.Interface); err != nil {
e.Logger.Info("shutting down the server")
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 10 seconds.
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
fmt.Println("Shutting down...")
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
}

57
models/config.go Normal file

@ -0,0 +1,57 @@
package models
import (
"github.com/go-ini/ini"
"os"
)
// ConfigStruct holds the config struct
type ConfigStruct struct {
Database struct {
Type string
Host string
User string
Password string
Database string
Path string
ShowQueries bool
}
JWTLoginSecret []byte
Interface string
}
// Config holds the configuration for the program
var Config = new(ConfigStruct)
// SetConfig initianlises the config and publishes it for other functions to use
func SetConfig() (err error) {
// File Checks
if _, err := os.Stat("config.ini"); os.IsNotExist(err) {
return err
}
// Load the config
cfg, err := ini.Load("config.ini")
if err != nil {
return err
}
// Map the config to our struct
err = cfg.MapTo(Config)
if err != nil {
return err
}
// Set default value for interface to listen on
Config.Interface = cfg.Section("General").Key("Interface").String()
if Config.Interface == "" {
Config.Interface = ":8080"
}
// JWT secret
Config.JWTLoginSecret = []byte(cfg.Section("General").Key("JWTSecret").String())
return nil
}

61
models/config_test.go Normal file

@ -0,0 +1,61 @@
package models
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"testing"
)
func TestSetConfig(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// This should fail as it is looking for a nonexistent config
err := SetConfig()
assert.Error(t, err)
// Write an invalid config
configString := `[General
JWTSecret = Supersecret
Interface = ; This should make it automatically to :8080
[Database
Type = sqlite
Path = ./library.db`
err = ioutil.WriteFile("config.ini", []byte(configString), 0644)
assert.NoError(t, err)
// Test setConfig (should fail as we're trying to parse an invalid config)
err = SetConfig()
assert.Error(t, err)
// Delete the invalid file
err = os.Remove("config.ini")
assert.NoError(t, err)
// Write a fake config
configString = `[General]
JWTSecret = Supersecret
Interface = ; This should make it automatically to :8080
[Database]
Type = sqlite
Path = ./library.db`
err = ioutil.WriteFile("config.ini", []byte(configString), 0644)
assert.NoError(t, err)
// Test setConfig
err = SetConfig()
assert.NoError(t, err)
// Check for the values
assert.Equal(t, []byte("Supersecret"), Config.JWTLoginSecret)
assert.Equal(t, string(":8080"), Config.Interface)
assert.Equal(t, string("sqlite"), Config.Database.Type)
assert.Equal(t, string("./library.db"), Config.Database.Path)
// Remove the dummy config
err = os.Remove("config.ini")
assert.NoError(t, err)
}

177
models/error.go Normal file

@ -0,0 +1,177 @@
package models
import "fmt"
// =====================
// User Operation Errors
// =====================
// ErrUsernameExists represents a "UsernameAlreadyExists" kind of error.
type ErrUsernameExists struct {
UserID int64
Username string
}
// IsErrUsernameExists checks if an error is a ErrUsernameExists.
func IsErrUsernameExists(err error) bool {
_, ok := err.(ErrUsernameExists)
return ok
}
func (err ErrUsernameExists) Error() string {
return fmt.Sprintf("a user with this username does already exist [user id: %d, username: %s]", err.UserID, err.Username)
}
// ErrUserEmailExists represents a "UserEmailExists" kind of error.
type ErrUserEmailExists struct {
UserID int64
Email string
}
// IsErrUserEmailExists checks if an error is a ErrUserEmailExists.
func IsErrUserEmailExists(err error) bool {
_, ok := err.(ErrUserEmailExists)
return ok
}
func (err ErrUserEmailExists) Error() string {
return fmt.Sprintf("a user with this email does already exist [user id: %d, email: %s]", err.UserID, err.Email)
}
// ErrNoUsername represents a "UsernameAlreadyExists" kind of error.
type ErrNoUsername struct {
UserID int64
}
// IsErrNoUsername checks if an error is a ErrUsernameExists.
func IsErrNoUsername(err error) bool {
_, ok := err.(ErrNoUsername)
return ok
}
func (err ErrNoUsername) Error() string {
return fmt.Sprintf("you need to specify a username [user id: %d]", err.UserID)
}
// ErrNoUsernamePassword represents a "NoUsernamePassword" kind of error.
type ErrNoUsernamePassword struct{}
// IsErrNoUsernamePassword checks if an error is a ErrNoUsernamePassword.
func IsErrNoUsernamePassword(err error) bool {
_, ok := err.(ErrNoUsernamePassword)
return ok
}
func (err ErrNoUsernamePassword) Error() string {
return fmt.Sprintf("you need to specify a username and a password")
}
// ErrUserDoesNotExist represents a "UserDoesNotExist" kind of error.
type ErrUserDoesNotExist struct {
UserID int64
}
// IsErrUserDoesNotExist checks if an error is a ErrUserDoesNotExist.
func IsErrUserDoesNotExist(err error) bool {
_, ok := err.(ErrUserDoesNotExist)
return ok
}
func (err ErrUserDoesNotExist) Error() string {
return fmt.Sprintf("this user does not exist [user id: %d]", err.UserID)
}
// ErrCouldNotGetUserID represents a "ErrCouldNotGetUserID" kind of error.
type ErrCouldNotGetUserID struct{}
// IsErrCouldNotGetUserID checks if an error is a ErrCouldNotGetUserID.
func IsErrCouldNotGetUserID(err error) bool {
_, ok := err.(ErrCouldNotGetUserID)
return ok
}
func (err ErrCouldNotGetUserID) Error() string {
return fmt.Sprintf("could not get user ID")
}
// ErrCannotDeleteLastUser represents a "ErrCannotDeleteLastUser" kind of error.
type ErrCannotDeleteLastUser struct{}
// IsErrCannotDeleteLastUser checks if an error is a ErrCannotDeleteLastUser.
func IsErrCannotDeleteLastUser(err error) bool {
_, ok := err.(ErrCannotDeleteLastUser)
return ok
}
func (err ErrCannotDeleteLastUser) Error() string {
return fmt.Sprintf("cannot delete last user")
}
// ===================
// Empty things errors
// ===================
// ErrIDCannotBeZero represents a "IDCannotBeZero" kind of error. Used if an ID (of something, not defined) is 0 where it should not.
type ErrIDCannotBeZero struct{}
// IsErrIDCannotBeZero checks if an error is a ErrIDCannotBeZero.
func IsErrIDCannotBeZero(err error) bool {
_, ok := err.(ErrIDCannotBeZero)
return ok
}
func (err ErrIDCannotBeZero) Error() string {
return fmt.Sprintf("ID cannot be 0")
}
// ErrAuthorCannotBeEmpty represents a "AuthorCannotBeEmpty" kind of error.
type ErrAuthorCannotBeEmpty struct{}
// IsErrAuthorCannotBeEmpty checks if an error is a ErrAuthorCannotBeEmpty.
func IsErrAuthorCannotBeEmpty(err error) bool {
_, ok := err.(ErrAuthorCannotBeEmpty)
return ok
}
func (err ErrAuthorCannotBeEmpty) Error() string {
return fmt.Sprintf("author cannot be empty")
}
// ErrItemTitleCannotBeEmpty represents a "ErrItemTitleCannotBeEmpty" kind of error.
type ErrItemTitleCannotBeEmpty struct{}
// IsErrItemTitleCannotBeEmpty checks if an error is a ErrItemTitleCannotBeEmpty.
func IsErrItemTitleCannotBeEmpty(err error) bool {
_, ok := err.(ErrItemTitleCannotBeEmpty)
return ok
}
func (err ErrItemTitleCannotBeEmpty) Error() string {
return fmt.Sprintf("title cannot be empty")
}
// ErrBookTitleCannotBeEmpty represents a "ErrBookTitleCannotBeEmpty" kind of error.
type ErrBookTitleCannotBeEmpty struct{}
// IsErrBookTitleCannotBeEmpty checks if an error is a ErrBookTitleCannotBeEmpty.
func IsErrBookTitleCannotBeEmpty(err error) bool {
_, ok := err.(ErrBookTitleCannotBeEmpty)
return ok
}
func (err ErrBookTitleCannotBeEmpty) Error() string {
return fmt.Sprintf("the book should at least have a title")
}
// ErrNoPublisherName represents a "ErrNoPublisherName" kind of error.
type ErrNoPublisherName struct{}
// IsErrNoPublisherName checks if an error is a ErrNoPublisherName.
func IsErrNoPublisherName(err error) bool {
_, ok := err.(ErrNoPublisherName)
return ok
}
func (err ErrNoPublisherName) Error() string {
return fmt.Sprintf("you need at least a name to insert a new publisher")
}

@ -0,0 +1,7 @@
-
id: 1
name: 'John Doe'
username: 'user1'
password: '1234'
email: 'johndoe@example.com'
is_admin: true

7
models/main_test.go Normal file

@ -0,0 +1,7 @@
package models
import "testing"
func TestMain(m *testing.M) {
MainTest(m, "..")
}

6
models/message.go Normal file

@ -0,0 +1,6 @@
package models
// Message is a standard message
type Message struct {
Message string `json:"message"`
}

48
models/models.go Normal file

@ -0,0 +1,48 @@
package models
import (
"fmt"
_ "github.com/go-sql-driver/mysql" // Because.
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
_ "github.com/mattn/go-sqlite3" // Because.
)
var x *xorm.Engine
func getEngine() (*xorm.Engine, error) {
// Use Mysql if set
if Config.Database.Type == "mysql" {
connStr := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=true",
Config.Database.User, Config.Database.Password, Config.Database.Host, Config.Database.Database)
return xorm.NewEngine("mysql", connStr)
}
// Otherwise use sqlite
path := Config.Database.Path
if path == "" {
path = "./db.db"
}
return xorm.NewEngine("sqlite3", path)
}
// SetEngine sets the xorm.Engine
func SetEngine() (err error) {
x, err = getEngine()
if err != nil {
return fmt.Errorf("Failed to connect to database: %v", err)
}
// Cache
cacher := xorm.NewLRUCacher(xorm.NewMemoryStore(), 1000)
x.SetDefaultCacher(cacher)
x.SetMapper(core.GonicMapper{})
// Sync dat shit
x.Sync(&User{})
x.ShowSQL(Config.Database.ShowQueries)
return nil
}

12
models/models_test.go Normal file

@ -0,0 +1,12 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestSetEngine(t *testing.T) {
Config.Database.Path = "file::memory:?cache=shared"
err := SetEngine()
assert.NoError(t, err)
}

19
models/test_fixtures.go Normal file

@ -0,0 +1,19 @@
package models
import (
"gopkg.in/testfixtures.v2"
)
var fixtures *testfixtures.Context
// InitFixtures initialize test fixtures for a test database
func InitFixtures(helper testfixtures.Helper, dir string) (err error) {
testfixtures.SkipDatabaseNameCheck(true)
fixtures, err = testfixtures.NewFolder(x.DB().DB, helper, dir)
return err
}
// LoadFixtures load fixtures for a test database
func LoadFixtures() error {
return fixtures.Load()
}

48
models/unit_tests.go Normal file

@ -0,0 +1,48 @@
package models
import (
"fmt"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
"gopkg.in/testfixtures.v2"
"os"
"path/filepath"
"testing"
)
// MainTest creates the test engine
func MainTest(m *testing.M, pathToRoot string) {
var err error
fixturesDir := filepath.Join(pathToRoot, "models", "fixtures")
if err = createTestEngine(fixturesDir); err != nil {
fmt.Fprintf(os.Stderr, "Error creating test engine: %v\n", err)
os.Exit(1)
}
os.Exit(m.Run())
}
func createTestEngine(fixturesDir string) error {
var err error
x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared")
//x, err = xorm.NewEngine("sqlite3", "db.db")
if err != nil {
return err
}
x.SetMapper(core.GonicMapper{})
// Sync dat shit
x.Sync(&User{})
// Show SQL-Queries if nessecary
if os.Getenv("UNIT_TESTS_VERBOSE") == "1" {
x.ShowSQL(true)
}
return InitFixtures(&testfixtures.SQLite{}, fixturesDir)
}
// PrepareTestDatabase load test fixtures into test database
func PrepareTestDatabase() error {
return LoadFixtures()
}

93
models/user.go Normal file

@ -0,0 +1,93 @@
package models
import (
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"golang.org/x/crypto/bcrypt"
)
// UserLogin Object to recive user credentials in JSON format
type UserLogin struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
}
// User holds information about an user
type User struct {
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
Name string `xorm:"varchar(250)" json:"name"`
Username string `xorm:"varchar(250) not null unique" json:"username"`
Password string `xorm:"varchar(250) not null" json:"password"`
Email string `xorm:"varchar(250)" json:"email"`
IsAdmin bool `xorm:"tinyint(1) not null" json:"isAdmin"`
Created int64 `xorm:"created" json:"created"`
Updated int64 `xorm:"updated" json:"updated"`
}
// TableName returns the table name for users
func (User) TableName() string {
return "users"
}
// GetUserByID gets informations about a user by its ID
func GetUserByID(id int64) (user User, exists bool, err error) {
// Apparently xorm does otherwise look for all users but return only one, which leads to returing one even if the ID is 0
if id == 0 {
return User{}, false, nil
}
return GetUser(User{ID: id})
}
// GetUser gets a user object
func GetUser(user User) (userOut User, exists bool, err error) {
userOut = user
exists, err = x.Get(&userOut)
if !exists {
return User{}, false, ErrUserDoesNotExist{}
}
return userOut, exists, err
}
// CheckUserCredentials checks user credentials
func CheckUserCredentials(u *UserLogin) (User, error) {
// Check if the user exists
user, exists, err := GetUser(User{Username: u.Username})
if err != nil {
return User{}, err
}
if !exists {
return User{}, ErrUserDoesNotExist{}
}
// Check the users password
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password))
if err != nil {
return User{}, err
}
return user, nil
}
// GetCurrentUser returns the current user based on its jwt token
func GetCurrentUser(c echo.Context) (user User, err error) {
jwtinf := c.Get("user").(*jwt.Token)
claims := jwtinf.Claims.(jwt.MapClaims)
userID, ok := claims["id"].(float64)
if !ok {
return user, ErrCouldNotGetUserID{}
}
user = User{
ID: int64(userID),
Name: claims["name"].(string),
Email: claims["email"].(string),
Username: claims["username"].(string),
}
return
}

125
models/user_add_update.go Normal file

@ -0,0 +1,125 @@
package models
import (
"golang.org/x/crypto/bcrypt"
)
// CreateUser creates a new user and inserts it into the database
func CreateUser(user User, doer *User) (newUser User, err error) {
newUser = user
// Check if we have all needed informations
if newUser.Password == "" || newUser.Username == "" {
return User{}, ErrNoUsernamePassword{}
}
// Check if the user already existst with that username
existingUser, exists, err := GetUser(User{Username: newUser.Username})
if err != nil {
return User{}, err
}
if exists {
return User{}, ErrUsernameExists{existingUser.ID, existingUser.Username}
}
// Check if the user already existst with that email
existingUser, exists, err = GetUser(User{Email: newUser.Email})
if err != nil {
return User{}, err
}
if exists {
return User{}, ErrUserEmailExists{existingUser.ID, existingUser.Email}
}
// Hash the password
newUser.Password, err = hashPassword(user.Password)
if err != nil {
return User{}, err
}
// Insert it
_, err = x.Insert(newUser)
if err != nil {
return User{}, err
}
// Get the full new User
newUserOut, _, err := GetUser(newUser)
if err != nil {
return User{}, err
}
return newUserOut, err
}
// HashPassword hashes a password
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
// UpdateUser updates a user
func UpdateUser(user User, doer *User) (updatedUser User, err error) {
// Check if it exists
theUser, exists, err := GetUserByID(user.ID)
if err != nil {
return User{}, err
}
if exists {
// Check if we have at least a username
if user.Username == "" {
//return User{}, ErrNoUsername{user.ID}
user.Username = theUser.Username // Dont change the username if we dont have one
}
user.Password = theUser.Password // set the password to the one in the database to not accedently resetting it
// Update it
_, err = x.Id(user.ID).Update(user)
if err != nil {
return User{}, err
}
// Get the newly updated user
updatedUser, _, err = GetUserByID(user.ID)
if err != nil {
return User{}, err
}
return updatedUser, err
}
return User{}, ErrUserDoesNotExist{user.ID}
}
// UpdateUserPassword updates the password of a user
func UpdateUserPassword(userID int64, newPassword string, doer *User) (err error) {
// Get all user details
user, exists, err := GetUserByID(userID)
if err != nil {
return err
}
if !exists {
return ErrUserDoesNotExist{userID}
}
// Hash the new password and set it
hashed, err := hashPassword(newPassword)
if err != nil {
return err
}
user.Password = hashed
// Update it
_, err = x.Id(user.ID).Update(user)
if err != nil {
return err
}
return err
}

28
models/user_delete.go Normal file

@ -0,0 +1,28 @@
package models
// DeleteUserByID deletes a user by its ID
func DeleteUserByID(id int64, doer *User) error {
// Check if the id is 0
if id == 0 {
return ErrIDCannotBeZero{}
}
// Check if there is > 1 user
total, err := x.Count(User{})
if err != nil {
return err
}
if total < 2 {
return ErrCannotDeleteLastUser{}
}
// Delete the user
_, err = x.Id(id).Delete(&User{})
if err != nil {
return err
}
return err
}

147
models/user_test.go Normal file

@ -0,0 +1,147 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestCreateUser(t *testing.T) {
// Create test database
assert.NoError(t, PrepareTestDatabase())
// Get our doer
doer, _, err := GetUserByID(1)
assert.NoError(t, err)
// Our dummy user for testing
dummyuser := User{
Name: "noooem, dief",
Username: "testuu",
Password: "1234",
Email: "noone@example.com",
IsAdmin: true,
}
// Delete every preexisting user to have a fresh start
_, err = x.Where("1 = 1").Delete(&User{})
assert.NoError(t, err)
allusers, err := ListUsers("")
assert.NoError(t, err)
for _, user := range allusers {
// Delete it
err := DeleteUserByID(user.ID, &doer)
assert.NoError(t, err)
}
// Create a new user
createdUser, err := CreateUser(dummyuser, &doer)
assert.NoError(t, err)
// Create a second new user
createdUser2, err := CreateUser(User{Username: dummyuser.Username + "2", Email: dummyuser.Email + "m", Password: dummyuser.Password}, &doer)
assert.NoError(t, err)
// Check if it fails to create the same user again
_, err = CreateUser(dummyuser, &doer)
assert.Error(t, err)
// Check if it fails to create a user with just the same username
_, err = CreateUser(User{Username: dummyuser.Username, Password: "fsdf"}, &doer)
assert.Error(t, err)
assert.True(t, IsErrUsernameExists(err))
// Check if it fails to create one with the same email
_, err = CreateUser(User{Username: "noone", Password: "1234", Email: dummyuser.Email}, &doer)
assert.Error(t, err)
assert.True(t, IsErrUserEmailExists(err))
// Check if it fails to create a user without password and username
_, err = CreateUser(User{}, &doer)
assert.Error(t, err)
assert.True(t, IsErrNoUsernamePassword(err))
_, err = CreateUser(User{Name: "blub"}, &doer)
assert.Error(t, err)
assert.True(t, IsErrNoUsernamePassword(err))
// Check if he exists
theuser, exists, err := GetUser(createdUser)
assert.NoError(t, err)
assert.True(t, exists)
// Get by his ID
_, exists, err = GetUserByID(theuser.ID)
assert.NoError(t, err)
assert.True(t, exists)
// Passing 0 as ID should return an empty user
_, exists, err = GetUserByID(0)
assert.NoError(t, err)
assert.False(t, exists)
// Check the user credentials
user, err := CheckUserCredentials(&UserLogin{"testuu", "1234"})
assert.NoError(t, err)
assert.Equal(t, dummyuser.Name, user.Name)
// Check wrong password (should also fail)
_, err = CheckUserCredentials(&UserLogin{"testuu", "12345"})
assert.Error(t, err)
// Check usercredentials for a nonexistent user (should fail)
_, err = CheckUserCredentials(&UserLogin{"dfstestuu", "1234"})
assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err))
// Update the user
newname := "Test_te"
uuser, err := UpdateUser(User{ID: theuser.ID, Name: newname, Password: "444444"}, &doer)
assert.NoError(t, err)
assert.Equal(t, newname, uuser.Name)
assert.Equal(t, theuser.Password, uuser.Password) // Password should not change
assert.Equal(t, theuser.Username, uuser.Username) // Username should not change either
// Try updating one which does not exist
_, err = UpdateUser(User{ID: 99999, Username: "dg"}, &doer)
assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err))
// Update a users password
newpassword := "55555"
err = UpdateUserPassword(theuser.ID, newpassword, &doer)
assert.NoError(t, err)
// Check if it was changed
user, err = CheckUserCredentials(&UserLogin{theuser.Username, newpassword})
assert.NoError(t, err)
assert.Equal(t, newname, user.Name)
// Check if the searchterm works
all, err := ListUsers("test")
assert.NoError(t, err)
assert.True(t, len(all) > 0)
all, err = ListUsers("")
assert.NoError(t, err)
assert.True(t, len(all) > 0)
// Try updating the password of a nonexistent user (should fail)
err = UpdateUserPassword(9999, newpassword, &doer)
assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err))
// Delete it
err = DeleteUserByID(theuser.ID, &doer)
assert.NoError(t, err)
// Try deleting one with ID = 0
err = DeleteUserByID(0, &doer)
assert.Error(t, err)
assert.True(t, IsErrIDCannotBeZero(err))
// Try delete the last user (Should fail)
err = DeleteUserByID(createdUser2.ID, &doer)
assert.Error(t, err)
assert.True(t, IsErrCannotDeleteLastUser(err))
}

25
models/users_list.go Normal file

@ -0,0 +1,25 @@
package models
// ListUsers returns a list with all users, filtered by an optional searchstring
func ListUsers(searchterm string) (users []User, err error) {
if searchterm == "" {
err = x.Find(&users)
} else {
err = x.
Where("username LIKE ?", "%"+searchterm+"%").
Or("name LIKE ?", "%"+searchterm+"%").
Find(&users)
}
// Obfuscate the password. Selecting everything except the password didn't work.
for i := range users {
users[i].Password = ""
}
if err != nil {
return []User{}, err
}
return users, nil
}

@ -0,0 +1,18 @@
package v1
import (
"fmt"
"git.kolaente.de/konrad/list/models"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
)
// CheckToken checks prints a message if the token is valid or not. Currently only used for testing pourposes.
func CheckToken(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
fmt.Println(user.Valid)
return c.JSON(418, models.Message{"🍵"})
}

@ -0,0 +1,101 @@
package v1
import (
"encoding/json"
"git.kolaente.de/konrad/list/models"
"github.com/labstack/echo"
"net/http"
"strconv"
"strings"
)
// UserAddOrUpdate is the handler to add a user
func UserAddOrUpdate(c echo.Context) error {
// TODO: prevent everyone from updating users
// Check for Request Content
userFromString := c.FormValue("user")
var datUser *models.User
if userFromString == "" {
// b := new(models.User)
if err := c.Bind(&datUser); err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"No user model provided."})
}
} else {
// Decode the JSON
dec := json.NewDecoder(strings.NewReader(userFromString))
err := dec.Decode(&datUser)
if err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"Error decoding user: " + err.Error()})
}
}
// Check if we have an ID other than the one in the struct
id := c.Param("id")
if id != "" {
// Make int
userID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"Invalid ID."})
}
datUser.ID = userID
}
// Check if the user exists
_, exists, err := models.GetUserByID(datUser.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not check if the user exists."})
}
// Get the doer options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Insert or update the user
var newUser models.User
if exists {
newUser, err = models.UpdateUser(*datUser, &doer)
} else {
newUser, err = models.CreateUser(*datUser, &doer)
}
if err != nil {
// Check for user already exists
if models.IsErrUsernameExists(err) {
return c.JSON(http.StatusBadRequest, models.Message{"A user with this username already exists."})
}
// Check for user with that email already exists
if models.IsErrUserEmailExists(err) {
return c.JSON(http.StatusBadRequest, models.Message{"A user with this email address already exists."})
}
// Check for no username provided
if models.IsErrNoUsername(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Please specify a username."})
}
// Check for no username or password provided
if models.IsErrNoUsernamePassword(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Please specify a username and a password."})
}
// Check for user does not exist
if models.IsErrUserDoesNotExist(err) {
return c.JSON(http.StatusBadRequest, models.Message{"The user does not exist."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Error"})
}
// Obfuscate his password
newUser.Password = ""
return c.JSON(http.StatusOK, newUser)
}

@ -0,0 +1,57 @@
package v1
import (
"git.kolaente.de/konrad/list/models"
"github.com/labstack/echo"
"net/http"
"strconv"
)
// UserDelete is the handler to delete a user
func UserDelete(c echo.Context) error {
// TODO: only allow users to allow itself
id := c.Param("id")
// Make int
userID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, models.Message{"User ID is invalid."})
}
// Check if the user exists
_, exists, err := models.GetUserByID(userID)
if err != nil {
return c.JSON(http.StatusInternalServerError, models.Message{"Could not get user."})
}
if !exists {
return c.JSON(http.StatusNotFound, models.Message{"The user does not exist."})
}
// Get the doer options
doer, err := models.GetCurrentUser(c)
if err != nil {
return err
}
// Delete it
err = models.DeleteUserByID(userID, &doer)
if err != nil {
if models.IsErrIDCannotBeZero(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Id cannot be 0"})
}
if models.IsErrCannotDeleteLastUser(err) {
return c.JSON(http.StatusBadRequest, models.Message{"Cannot delete last user."})
}
return c.JSON(http.StatusInternalServerError, models.Message{"Could not delete user."})
}
return c.JSON(http.StatusOK, models.Message{"success"})
}

@ -0,0 +1,43 @@
package v1
import (
"git.kolaente.de/konrad/list/models"
"github.com/labstack/echo"
"net/http"
"strconv"
)
// UserShow gets all informations about a user
func UserShow(c echo.Context) error {
// TODO: only allow users to show itself/with privacy options
user := c.Param("id")
if user == "" {
return c.JSON(http.StatusBadRequest, models.Message{"User ID cannot be empty."})
}
// Make int