Compare commits

..

22 Commits
v0.2 ... v0.3

Author SHA1 Message Date
konrad
14207a0fa8 Fix tests in drone (#13) 2018-11-02 14:37:11 +00:00
konrad
e906184bf7 updated version in readme 2018-11-02 15:29:01 +01:00
kolaente
1f41db9239 Hotfix: fix duplicate header in html mails with templates 2018-11-02 11:23:41 +01:00
konrad
4f8a0f8739 Fixed no root path for templates and static assets (#12) 2018-11-02 10:01:28 +00:00
konrad
c51ca4dd26 Fixed dockerfile (#11) 2018-11-02 09:18:38 +00:00
konrad
9e539b8c40 Fix checking for an email verification token (#10) 2018-11-01 22:59:20 +00:00
konrad
9a5eb577b8 Fix not checking for user credentials upon login (#9) 2018-11-01 22:51:05 +00:00
konrad
4713023a97 Prevent login from inactive (aka non-verified) users (#8) 2018-11-01 22:47:41 +00:00
konrad
301a4eedda New structure (#7) 2018-10-31 12:42:38 +00:00
kolaente
3f9fad0e2a use go modules from vendor when building the docker image 2018-10-28 18:38:40 +01:00
kolaente
990740fc19 fixed fmt 2018-10-28 17:35:48 +01:00
kolaente
960d72aa1b Fixed tests 2018-10-28 17:30:10 +01:00
kolaente
f94bbf023c Merge branch 'master' of ssh://git.kolaente.de:9022/vikunja/api 2018-10-28 17:24:13 +01:00
kolaente
2556ef7624 added vendor 2018-10-28 17:24:10 +01:00
konrad
321c0f2404 Fix build when using go modules (#6) 2018-10-28 16:11:13 +00:00
konrad
738c22b5f9 Add user email verification when registering (#5) 2018-10-27 13:14:55 +00:00
konrad
d0c30cb089 Password reset with token only (#4) 2018-10-27 13:12:15 +00:00
konrad
0cfea682ea Add password reset (#3) 2018-10-27 09:33:28 +00:00
konrad
95197ec6ed moar featurecreep 2018-10-22 22:20:45 +02:00
konrad
eecace511d updated todo 2018-10-21 22:22:56 +02:00
konrad
6793e13e6d updated todo 2018-10-21 22:21:37 +02:00
konrad
85da2d72aa updated todo 2018-10-21 22:19:51 +02:00
915 changed files with 194115 additions and 16216 deletions

View File

@@ -10,17 +10,15 @@ clone:
pipeline:
build:
image: webhippie/golang:edge
image: vikunja/golang-build:latest
pull: true
group: build
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make clean
- make lint
- make fmt-check
- make swagger-check
# - make swagger-check # Inactive until go-swagger works with go modules
- make ineffassign-check
- make misspell-check
- make build
@@ -28,10 +26,8 @@ pipeline:
event: [ push, tag, pull_request ]
test:
image: webhippie/golang:edge
image: vikunja/golang-build:latest
pull: true
environment:
GOPATH: /srv/app
commands:
- make test
when:

View File

@@ -5,6 +5,8 @@ FROM golang:1.11-alpine3.7 AS build-env
ARG VIKUNJA_VERSION
ENV TAGS "sqlite"
ENV GO111MODULE=on
ENV GOFLAGS=-mod=vendor
#Build deps
RUN apk --no-cache add build-base git
@@ -34,8 +36,11 @@ RUN apk --no-cache add \
tzdata
COPY docker /
COPY --from=build-env /go/src/code.vikunja.io/api/public /app/vikunja/
COPY --from=build-env /go/src/code.vikunja.io/api/public /app/vikunja/public
COPY --from=build-env /go/src/code.vikunja.io/api/templates /app/vikunja/templates
COPY --from=build-env /go/src/code.vikunja.io/api/vikunja /app/vikunja/vikunja
ENV VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/
ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
CMD []

View File

@@ -167,6 +167,9 @@ Teams sind global, d.h. Ein Team kann mehrere Namespaces verwalten.
* [ ] Attachments
* [ ] Repeating tasks
* [ ] Tagesübersicht ("Was ist heute/diese Woche due?") -> Machen letztenendes die Clients, wir brauchen nur nen endpoint, der alle tasks auskotzt, der Client macht dann die Sortierung.
* [ ] Mit definierbarem Bereich, sollte aber trotzdem der server machen, so à la "Gib mir alles für diesen Monat"
* [ ] Namespaces in Namespaces (in Namespaces in Namespaces in Namespaces...)
* [ ] Rechtemanagement dafür wird schwierig
## Clients
@@ -212,13 +215,27 @@ Teams sind global, d.h. Ein Team kann mehrere Namespaces verwalten.
* [x] namespaces & listen updaten geht nicht, gibt nen 500er zurück
* [x] Logging für alle Fehler irgendwohin, da gibts bestimmt ne coole library für
* [x] Ne extra funktion für list exists machen, damit die nicht immer über GetListByID gehen, um sql-abfragen zu sparen
* [ ] Search endpoints /users?s=name und /teams?s=name, erstmal nur mit Namen suchen.
* [ ] Userstuff aufräumen, auf neuen Handler umziehen?
* [x] Rausfinden warum xorm teilweise beim einfügen IDs mit einfügen will -> Das schlägt dann wegen duplicate fehl
* [x] Bei den Structs "AfterLoad" raus, das verbraucht bei Gruppenabfragen zu viele SQL-Abfragen -> Die sollen einfach die entsprechenden Read()-Methoden verwenden (Krassestes bsp. ist GET /namespaces mit so ca 50 Abfragen)
* [ ] Search endpoints /users?s=name und /teams?s=name, erstmal nur mit Namen suchen.
-> Search methode in den Handler einbauen und dann die Endpoints entsprechend anpassen
-> Macht Sinn das in ne eigene Methode umzubauen weil der query dazu leicht anders ist und man das auch nicht sooo einfach integrieren kann (so nach dem Motto einfach nen Searchstring anhängen)
* [ ] Methode einbauen, um mit einem gültigen token ein neues gültiges zu kriegen
* [ ] Wir brauchen noch ne gute idee, wie man die listen kriegt, auf die man nur so Zugriff hat (ohne namespace)
* [ ] Validation der ankommenden structs, am besten mit https://github.com/go-validator/validator
* [ ] Rausfinden warum xorm teilweise beim einfügen IDs mit einfügen will -> Das schlägt dann wegen duplicate fehl
* [ ] Pagination
* Sollte in der Config definierbar sein, wie viel pro Seite angezeigt werden soll, die CRUD-Methoden übergeben dann ein "gibt mir die Seite sowieso" an die CRUDable-Funktionenen, die müssen das dann Auswerten. Geht leider nicht anders, wenn man erst 2342352 Einträge hohlt und die dann nachträglich auf 200 begrenzt ist das ne massive Ressourcenverschwendung.
* [ ] Testing mit locust: https://locust.io/
#### Userstuff
* [ ] Userstuff aufräumen
-> Soweit es geht und Sinnvoll ist auf den neuen Handler umziehen
-> Login/Register/Password-reset geht natürlich nicht
-> Bleibt noch Profile abrufen und Einstellungen -> Macht also keinen Sinn das auf den neuen Handler umzuziehen
* [x] Email-Verifizierung beim Registrieren
* [x] Password Reset -> Link via email oder so
* [ ] Settings
### Later/Nice to have

324
Gopkg.lock generated
View File

@@ -1,324 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "UT"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
digest = "1:6cb5bf34f3e3bb61e76d976f5bd29d245b567f208e1fd7cb8104b736a5eb649d"
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
pruneopts = "UT"
revision = "a539ee1a749a2b895533f979515ac7e6e0f5b650"
[[projects]]
digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd"
name = "github.com/fsnotify/fsnotify"
packages = ["."]
pruneopts = "UT"
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
digest = "1:0594af97b2f4cec6554086eeace6597e20a4b69466eb4ada25adf9f4300dddd2"
name = "github.com/garyburd/redigo"
packages = [
"internal",
"redis",
]
pruneopts = "UT"
revision = "a69d19351219b6dd56f274f96d85a7014a2ec34e"
version = "v1.6.0"
[[projects]]
digest = "1:d2b67246c3fa959edfa1be84b407cf0a0a4cabbace0843814b1729b5af808d9e"
name = "github.com/go-sql-driver/mysql"
packages = ["."]
pruneopts = "UT"
revision = "ee359f95877bdef36cbb602711e49b6f0becfca9"
[[projects]]
digest = "1:3f854136f2da9cde22fa0dd29e74fb7c920c5d79d060847fe9b96f2d3cfab746"
name = "github.com/go-xorm/builder"
packages = ["."]
pruneopts = "UT"
revision = "c8871c857d2555fbfbd8524f895be5386d3d8836"
[[projects]]
digest = "1:ee0b218753df985dd6633b13a894a69f0c1ea385f092f4afa093d407bb165cc5"
name = "github.com/go-xorm/core"
packages = ["."]
pruneopts = "UT"
revision = "f43c33d9a48db006417a7ac4c16b08897e3e1458"
version = "v0.5.8"
[[projects]]
digest = "1:8e0764e74dd6cf539d3c035772d3fd0009b89fff9889a1131139be6d871c90dd"
name = "github.com/go-xorm/xorm"
packages = ["."]
pruneopts = "UT"
revision = "29d4a0330a00b9be468b70e3fb0f74109348c358"
[[projects]]
branch = "master"
digest = "1:d4a957ebe4ccebc299c1f6b6a78a0713af379721980cf14990b8ab8fa93cd92d"
name = "github.com/go-xorm/xorm-redis-cache"
packages = ["."]
pruneopts = "UT"
revision = "859b313566b2ef090319245fd4fe7692f25dbd79"
[[projects]]
digest = "1:c0d19ab64b32ce9fe5cf4ddceba78d5bc9807f0016db6b1183599da3dcc24d10"
name = "github.com/hashicorp/hcl"
packages = [
".",
"hcl/ast",
"hcl/parser",
"hcl/printer",
"hcl/scanner",
"hcl/strconv",
"hcl/token",
"json/parser",
"json/scanner",
"json/token",
]
pruneopts = "UT"
revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241"
version = "v1.0.0"
[[projects]]
digest = "1:8eb1de8112c9924d59bf1d3e5c26f5eaa2bfc2a5fcbb92dc1c2e4546d695f277"
name = "github.com/imdario/mergo"
packages = ["."]
pruneopts = "UT"
revision = "9f23e2d6bd2a77f959b2bf6acdbefd708a83a4a4"
version = "v0.3.6"
[[projects]]
digest = "1:82c5850e702ccef3976cdfdf737725013587c64e02e5ecae57e86431163b27aa"
name = "github.com/labstack/echo"
packages = [
".",
"middleware",
]
pruneopts = "UT"
revision = "1049c9613cd371b7ea8f219404c9a821734781ed"
version = "v3.1.0"
[[projects]]
digest = "1:ef6158d11f87c8036114cf3d2bf5c50edb4aa0b4aab5deef26716a8bb674a9ad"
name = "github.com/labstack/gommon"
packages = [
"bytes",
"color",
"log",
"random",
]
pruneopts = "UT"
revision = "57409ada9da0f2afad6664c49502f8c50fbd8476"
version = "0.2.3"
[[projects]]
digest = "1:c568d7727aa262c32bdf8a3f7db83614f7af0ed661474b24588de635c20024c7"
name = "github.com/magiconair/properties"
packages = ["."]
pruneopts = "UT"
revision = "c2353362d570a7bfa228149c62842019201cfb71"
version = "v1.8.0"
[[projects]]
digest = "1:a36a1febe1240bb79a208390ad17e1080555b0031a9ed42a41eae173cca3fd74"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "ad5389df28cdac544c99bd7b9161a0b5b6ca9d1b"
[[projects]]
digest = "1:d4d17353dbd05cb52a2a52b7fe1771883b682806f68db442b436294926bbfafb"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "a5cdd64afdee435007ee3e9f6ed4684af949d568"
[[projects]]
digest = "1:3cafc6a5a1b8269605d9df4c6956d43d8011fc57f266ca6b9d04da6c09dee548"
name = "github.com/mattn/go-sqlite3"
packages = ["."]
pruneopts = "UT"
revision = "25ecb14adfc7543176f7d85291ec7dba82c6f7e4"
version = "v1.9.0"
[[projects]]
digest = "1:645110e089152bd0f4a011a2648fbb0e4df5977be73ca605781157ac297f50c4"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
pruneopts = "UT"
revision = "fa473d140ef3c6adf42d6b391fe76707f1f243c8"
version = "v1.0.0"
[[projects]]
digest = "1:5b3b29ce0e569f62935d9541dff2e16cc09df981ebde48e82259076a73a3d0c7"
name = "github.com/op/go-logging"
packages = ["."]
pruneopts = "UT"
revision = "b2cb9fa56473e98db8caba80237377e83fe44db5"
version = "v1"
[[projects]]
digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e"
name = "github.com/pelletier/go-toml"
packages = ["."]
pruneopts = "UT"
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
version = "v1.2.0"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:6a4a11ba764a56d2758899ec6f3848d24698d48442ebce85ee7a3f63284526cd"
name = "github.com/spf13/afero"
packages = [
".",
"mem",
]
pruneopts = "UT"
revision = "d40851caa0d747393da1ffb28f7f9d8b4eeffebd"
version = "v1.1.2"
[[projects]]
digest = "1:516e71bed754268937f57d4ecb190e01958452336fa73dbac880894164e91c1f"
name = "github.com/spf13/cast"
packages = ["."]
pruneopts = "UT"
revision = "8965335b8c7107321228e3e3702cab9832751bac"
version = "v1.2.0"
[[projects]]
digest = "1:68ea4e23713989dc20b1bded5d9da2c5f9be14ff9885beef481848edd18c26cb"
name = "github.com/spf13/jwalterweatherman"
packages = ["."]
pruneopts = "UT"
revision = "4a4406e478ca629068e7768fc33f3f044173c0a6"
version = "v1.0.0"
[[projects]]
digest = "1:dab83a1bbc7ad3d7a6ba1a1cc1760f25ac38cdf7d96a5cdd55cd915a4f5ceaf9"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = "UT"
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
version = "v1.0.2"
[[projects]]
digest = "1:6e30a27eac59a148b3f7a32e0ba54706b31dcde5a42f63b22cb47873b62fa343"
name = "github.com/spf13/viper"
packages = ["."]
pruneopts = "UT"
revision = "8fb642006536c8d3760c99d4fa2389f5e2205631"
version = "v1.2.0"
[[projects]]
digest = "1:a7496c8d47f794d15a6e41f8ada426d4a41c038fac9bd75c1abc30faacf469c9"
name = "github.com/stretchr/testify"
packages = ["assert"]
pruneopts = "UT"
revision = "87b1dfb5b2fa649f52695dd9eae19abe404a4308"
[[projects]]
branch = "master"
digest = "1:c468422f334a6b46a19448ad59aaffdfc0a36b08fdcc1c749a0b29b6453d7e59"
name = "github.com/valyala/bytebufferpool"
packages = ["."]
pruneopts = "UT"
revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7"
[[projects]]
branch = "master"
digest = "1:268b8bce0064e8c057d7b913605459f9a26dcab864c0886a56d196540fbf003f"
name = "github.com/valyala/fasttemplate"
packages = ["."]
pruneopts = "UT"
revision = "dcecefd839c4193db0d35b88ec65b4c12d360ab0"
[[projects]]
digest = "1:456c96b87c9481d98aac8ef9862053a8fc97b981d26f6cd8d63de783dd24acec"
name = "golang.org/x/crypto"
packages = [
"acme",
"acme/autocert",
"bcrypt",
"blowfish",
]
pruneopts = "UT"
revision = "9419663f5a44be8b34ca85f08abc5fe1be11f8a3"
[[projects]]
digest = "1:f1275b0d1ed7d43eeb7a0464a148258f04e66a4783514d5c790d4e970cc7df6d"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "UT"
revision = "314a259e304ff91bd6985da2a7149bbf91237993"
[[projects]]
digest = "1:8029e9743749d4be5bc9f7d42ea1659471767860f0cdc34d37c3111bd308a295"
name = "golang.org/x/text"
packages = [
"internal/gen",
"internal/triegen",
"internal/ucd",
"transform",
"unicode/cldr",
"unicode/norm",
]
pruneopts = "UT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
digest = "1:59c645bd5db5ec4a889a1ac62ce9cb3c4483cc24c66907ea75de2516d6fe4716"
name = "gopkg.in/testfixtures.v2"
packages = ["."]
pruneopts = "UT"
revision = "1d98c34adfb14dbedeef37127968233b5d960f02"
version = "v2.4.5"
[[projects]]
digest = "1:73e6fda93622790d2371344759df06ff5ff2fac64a6b6e8832b792e7402956e7"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4"
version = "v2.0.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/dgrijalva/jwt-go",
"github.com/go-sql-driver/mysql",
"github.com/go-xorm/core",
"github.com/go-xorm/xorm",
"github.com/go-xorm/xorm-redis-cache",
"github.com/imdario/mergo",
"github.com/labstack/echo",
"github.com/labstack/echo/middleware",
"github.com/mattn/go-sqlite3",
"github.com/op/go-logging",
"github.com/spf13/viper",
"github.com/stretchr/testify/assert",
"golang.org/x/crypto/bcrypt",
"gopkg.in/testfixtures.v2",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -16,7 +16,7 @@ endif
GOFILES := $(shell find . -name "*.go" -type f ! -path "./vendor/*" ! -path "*/bindata.go")
GOFMT ?= gofmt -s
GOFLAGS := -i -v
GOFLAGS := -v -mod=vendor
EXTRA_GOFLAGS ?=
LDFLAGS := -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.Tags=$(TAGS)"
@@ -44,12 +44,14 @@ else
endif
endif
VERSION := $(shell echo $(VERSION) | sed 's/\//\-/g')
.PHONY: all
all: build
.PHONY: clean
clean:
go clean -i ./...
go clean ./...
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA)
.PHONY: test
@@ -58,12 +60,12 @@ test:
go tool cover -html=cover.out -o cover.html
required-gofmt-version:
@go version | grep -q '\(1.7\|1.8\|1.9\|1.10\|1.11\)' || { echo "We require go version 1.7, 1.8, 1.9 or 1.10 to format code" >&2 && exit 1; }
@go version | grep -q '\(1.7\|1.8\|1.9\|1.10\|1.11\)' || { echo "We require go version 1.7, 1.8, 1.9, 1.10 or 1.11 to format code" >&2 && exit 1; }
.PHONY: lint
lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u golang.org/x/lint/golint; \
go install $(GOFLAGS) golang.org/x/lint/golint; \
fi
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
@@ -101,7 +103,7 @@ release-dirs:
.PHONY: release-windows
release-windows:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/karalabe/xgo; \
go install $(GOFLAGS) github.com/karalabe/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out vikunja-$(VERSION) .
ifeq ($(CI),drone)
@@ -111,7 +113,7 @@ endif
.PHONY: release-linux
release-linux:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/karalabe/xgo; \
go install $(GOFLAGS) github.com/karalabe/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/*' -out vikunja-$(VERSION) .
ifeq ($(CI),drone)
@@ -121,7 +123,7 @@ endif
.PHONY: release-darwin
release-darwin:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/karalabe/xgo; \
go install $(GOFLAGS) github.com/karalabe/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out vikunja-$(VERSION) .
ifeq ($(CI),drone)
@@ -133,6 +135,8 @@ release-copy:
$(foreach file,$(wildcard $(DIST)/binaries/$(EXECUTABLE)-*),cp $(file) $(DIST)/release/$(notdir $(file));)
mkdir $(DIST)/release/public
cp public/ $(DIST)/release/ -R
mkdir $(DIST)/release/templates
cp templates/ $(DIST)/templates/ -R
.PHONY: release-check
release-check:
@@ -141,7 +145,7 @@ release-check:
.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.yml.sample $(file)-full/config.yml; cp $(DIST)/release/public $(file)-full/ -R; cp LICENSE $(file)-full/; )
$(foreach file,$(filter-out %.sha256,$(wildcard $(DIST)/release/$(EXECUTABLE)-*)),mkdir $(file)-full;mv $(file) $(file)-full/; mv $(file).sha256 $(file)-full/; cp config.yml.sample $(file)-full/config.yml; cp $(DIST)/release/public $(file)-full/ -R; cp $(DIST)/release/templates $(file)-full/ -R; cp LICENSE $(file)-full/; )
rm $(DIST)/release/public -rf
.PHONY: release-zip
@@ -151,7 +155,7 @@ release-zip:
.PHONY: generate-swagger
generate-swagger:
@hash swagger > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/go-swagger/go-swagger/cmd/swagger; \
go install $(GOFLAGS) github.com/go-swagger/go-swagger/cmd/swagger; \
fi
swagger generate spec -o ./public/swagger/swagger.v1.json
@@ -167,27 +171,27 @@ swagger-check: generate-swagger
.PHONY: swagger-validate
swagger-validate:
@hash swagger > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/go-swagger/go-swagger/cmd/swagger; \
go install $(GOFLAGS) github.com/go-swagger/go-swagger/cmd/swagger; \
fi
swagger validate ./public/swagger/swagger.v1.json
.PHONY: misspell-check
misspell-check:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/client9/misspell/cmd/misspell; \
go install $(GOFLAGS) github.com/client9/misspell/cmd/misspell; \
fi
for S in $(GOFILES); do misspell -error $$S || exit 1; done;
.PHONY: ineffassign-check
ineffassign-check:
@hash ineffassign > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/gordonklaus/ineffassign; \
go install $(GOFLAGS) github.com/gordonklaus/ineffassign; \
fi
for S in $(GOFILES); do ineffassign $$S || exit 1; done;
.PHONY: gocyclo-check
gocyclo-check:
@hash gocyclo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get github.com/fzipp/gocyclo; \
go install $(GOFLAGS) github.com/fzipp/gocyclo; \
fi
for S in $(GOFILES); do gocyclo -over 14 $$S || exit 1; done;
for S in $(GOFILES); do gocyclo -over 14 $$S || exit 1; done;

View File

@@ -4,7 +4,7 @@
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/api/status.svg)](https://drone.kolaente.de/vikunja/api)
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.1-brightgreen.svg)](https://storage.kolaente.de/minio/vikunja/)
[![Download](https://img.shields.io/badge/download-v0.3-brightgreen.svg)](https://storage.kolaente.de/minio/vikunja/)
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/api.svg)](https://hub.docker.com/r/vikunja/api/)
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/swagger)
[![Go Report Card](https://goreportcard.com/badge/git.kolaente.de/vikunja/api)](https://goreportcard.com/report/git.kolaente.de/vikunja/api
@@ -32,6 +32,8 @@ Try it under [try.vikunja.io](https://try.vikunja.io)!
## Development
We use go modules to vendor libraries for Vikunja, so you'll need at least go `1.11`.
To contribute to Vikunja, fork the project and work on the master branch.
Some internal packages are referenced using their respective package URL. This can become problematic. To “trick” the Go tool into thinking this is a clone from the official repository, download the source code into `$GOPATH/code.vikunja.io/api`. Fork the Vikunja repository, it should then be possible to switch the source directory on the command line.
@@ -44,7 +46,7 @@ To be able to create pull requests, the forked repository should be added as a r
```bash
git remote rename origin upstream
git remote add origin git@git.kolaente.de:<USERNAME>/vikunja.git
git remote add origin git@git.kolaente.de:<USERNAME>/api.git
git fetch --all --prune
```

View File

@@ -3,23 +3,21 @@ POST http://localhost:8080/api/v1/login
Content-Type: application/json
{
"user": "user",
"username": "user",
"password": "1234"
}
> {% client.global.set("auth_token", response.body.token); %}
###
## Register
### Register
POST http://localhost:8080/api/v1/register
Content-Type: application/json
{
"username": "user3",
"username": "user4",
"password": "1234",
"email": "3@knt.li"
"email": "4@knt.li"
}
###

View File

@@ -21,4 +21,33 @@ Content-Type: application/json
"new_password": "1234"
}
###
### Request a password to reset a password
POST http://localhost:8080/api/v1/user/password/token
Content-Type: application/json
Accept: application/json
{
"user_name": "user"
}
### Request a token to reset a password
POST http://localhost:8080/api/v1/user/password/reset
Content-Type: application/json
Accept: application/json
{
"token": "eAsZzakgqARnjzXHqsHqZtSUKuiOhoJjHANhgTxUIDBSalhbtdpAdLeywGXzVDBuRQGNpHdMxoHXhLVSlzpJsFvuoJgMdkhRhkNhaQXfufuZCdtUlerZHSJQLgYMUryHIxIREcmZLtWoZVrYyARkCvkyFhcGtoCwQOEjAOEZMQQuxTVoGYfAqcfNggQnerUcXCiRIgRtkusXSnltomhaeyRwAbrckXFeXxUjslgplSGqSTOqJTYuhrSzAVTwNvuYyvuXLaZoNnJEyeVDWlRydnxfgUQjQZOKwCBRWVQPKpZhlslLUyUAMsRQkHITkruQCjDnOGCCRsSNplbNCEuDmMfpWYHSQAcQIDZtbQWkxzpfmHDMQvvKPPrxEnrTErlvTfKDKICFYPQxXNpNE",
"new_password": "1234"
}
### Confirm a users email address
POST http://localhost:8080/api/v1/user/confirm
Content-Type: application/json
Accept: application/json
{
"token": ""
}
###

View File

@@ -5,6 +5,8 @@ service:
JWTSecret: "cei6gaezoosah2bao3ieZohkae5aicah"
# The interface on which to run the webserver
interface: ":3456"
# The URL of the frontend, used to send password reset emails.
frontendurl: ""
database:
# Database type to use. Supported types are mysql and sqlite.
@@ -33,3 +35,17 @@ cache:
redishost: 'localhost:6379'
# When using redis, this is the password used to authenicate against the redis server
redispassword: ''
mailer:
# SMTP Host
host: ''
# SMTP Host port
port: 587
# SMTP username
username: 'user'
# SMTP password
password: ''
# Wether to skip verification of the tls certificate on the server
skiptlsverify: false
# The default from address when sending emails
fromemail: 'mail@vikunja'

View File

@@ -29,6 +29,10 @@ service:
JWTSecret: "cei6gaezoosah2bao3ieZohkae5aicah"
# The interface on which to run the webserver
interface: ":3456"
# The URL of the frontend, used to send password reset emails.
frontendurl: ""
# The base path on the file system where the binary and assets are
rootpath: <the path of the executable>
database:
# Database type to use. Supported types are mysql and sqlite.
@@ -60,4 +64,22 @@ cache:
redishost: 'localhost:6379'
# When using redis, this is the password used to authenicate against the redis server
redispassword: ''
mailer:
# SMTP Host
host: ""
# SMTP Host port
port: 587
# SMTP username
username: "user"
# SMTP password
password: ""
# Wether to skip verification of the tls certificate on the server
skiptlsverify: false
# The default from address when sending emails
fromemail: "mail@vikunja"
# The length of the mail queue.
queuelength: 100
# The timeout in seconds after which the current open connection to the mailserver will be closed.
queuetimeout: 30
```

View File

@@ -9,7 +9,11 @@ This document describes the different errors Vikunja can return.
| 1004 | 400 | No username and password specified. |
| 1005 | 404 | The user does not exist. |
| 1006 | 400 | Could not get the user id. |
| 1007 | 409 | Cannot delete the last user on the system. |
| 1008 | 412 | No password reset token provided. |
| 1009 | 412 | Invalid password reset token. |
| 1010 | 412 | Invalid email confirm token. |
| 1011 | 412 | Wrong username or password. |
| 1012 | 412 | Email address of the user not confirmed. |
| 2001 | 400 | ID cannot be empty or 0. |
| 3001 | 404 | The list does not exist. |
| 3004 | 403 | The user needs to have read permissions on that list to perform that action. |

27
go.mod
View File

@@ -3,32 +3,53 @@ module code.vikunja.io/api
require (
cloud.google.com/go v0.30.0 // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/client9/misspell v0.3.4
github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 // indirect
github.com/dgrijalva/jwt-go v0.0.0-20170608005149-a539ee1a749a
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835
github.com/garyburd/redigo v1.6.0 // indirect
github.com/go-openapi/analysis v0.17.2 // indirect
github.com/go-openapi/errors v0.17.2 // indirect
github.com/go-openapi/inflect v0.17.2 // indirect
github.com/go-openapi/loads v0.17.2 // indirect
github.com/go-openapi/runtime v0.17.2 // indirect
github.com/go-openapi/spec v0.17.2 // indirect
github.com/go-openapi/strfmt v0.17.2 // indirect
github.com/go-openapi/swag v0.17.2 // indirect
github.com/go-openapi/validate v0.17.2 // indirect
github.com/go-sql-driver/mysql v0.0.0-20171007150158-ee359f95877b
github.com/go-swagger/go-swagger v0.17.2
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25 // indirect
github.com/go-xorm/core v0.5.8
github.com/go-xorm/tests v0.5.6 // indirect
github.com/go-xorm/xorm v0.0.0-20170930012613-29d4a0330a00
github.com/go-xorm/xorm-redis-cache v0.0.0-20180727005610-859b313566b2
github.com/google/go-cmp v0.2.0 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc
github.com/gorilla/handlers v1.4.0 // indirect
github.com/imdario/mergo v0.3.6
github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/joho/godotenv v1.3.0 // indirect
github.com/karalabe/xgo v0.0.0-20181007145344-72da7d1d3970
github.com/kr/pretty v0.1.0 // indirect
github.com/labstack/echo v3.1.0+incompatible
github.com/labstack/gommon v0.0.0-20170925052817-57409ada9da0 // indirect
github.com/labstack/gommon v0.0.0-20170925052817-57409ada9da0
github.com/lib/pq v1.0.0 // indirect
github.com/mattn/go-colorable v0.0.0-20170816031813-ad5389df28cd // indirect
github.com/mattn/go-isatty v0.0.0-20170925054904-a5cdd64afdee // indirect
github.com/mattn/go-oci8 v0.0.0-20181011085415-1a014d1384b5 // indirect
github.com/mattn/go-sqlite3 v1.9.0
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/viper v1.2.0
github.com/stretchr/testify v0.0.0-20171231124224-87b1dfb5b2fa
github.com/stretchr/testify v1.2.2
github.com/toqueteos/webbrowser v1.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3
golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/testfixtures.v2 v2.4.5
)

81
go.sum
View File

@@ -2,18 +2,64 @@ cloud.google.com/go v0.30.0 h1:xKvyLgk56d0nksWq49J0UyGEeUIicTl4+UBiX1NPX9g=
cloud.google.com/go v0.30.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
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/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 h1:BZGp1dbKFjqlGmxEpwkDpCWNxVwEYnUPoncIzLiHlPo=
github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
github.com/dgrijalva/jwt-go v0.0.0-20170608005149-a539ee1a749a h1:nmYyGtn9AO7FCeZ2tHr1ZWjJAHi6SfGB3o80F8o7EbA=
github.com/dgrijalva/jwt-go v0.0.0-20170608005149-a539ee1a749a/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 h1:roDmqJ4Qes7hrDOsWsMCce0vQHz3xiMPjJ9m4c2eeNs=
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835/go.mod h1:BjL/N0+C+j9uNX+1xcNuM9vdSIcXCZrQZUYbXOFbgN8=
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/analysis v0.17.2 h1:eYp14J1o8TTSCzndHBtsNuckikV1PfZOSnx4BcBeu0c=
github.com/go-openapi/analysis v0.17.2/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/errors v0.17.2 h1:azEQ8Fnx0jmtFF2fxsnmd6I0x6rsweUF63qqSO1NmKk=
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/inflect v0.17.2 h1:4Zg/XLOwxsyKGFHxCI9e4AkxUFSjLKaoY+jnStSJAfw=
github.com/go-openapi/inflect v0.17.2/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.17.2 h1:tEXYu6Xc0pevpzzQx5ghrMN9F7IVpN/+u4iD3rkYE5o=
github.com/go-openapi/loads v0.17.2/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
github.com/go-openapi/runtime v0.17.2 h1:/ZK67ikFhQAMFFH/aPu2MaGH7QjP4wHBvHYOVIzDAw0=
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.17.2 h1:eb2NbuCnoe8cWAxhtK6CfMWUYmiFEZJ9Hx3Z2WRwJ5M=
github.com/go-openapi/spec v0.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/strfmt v0.17.2 h1:2KDns36DMHXG9/iYkOjiX+/8fKK9GCU5ELZ+J6qcRVA=
github.com/go-openapi/strfmt v0.17.2/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.17.2 h1:K/ycE/XTUDFltNHSO32cGRUhrVGJD64o8WgAIZNyc3k=
github.com/go-openapi/swag v0.17.2/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/go-openapi/validate v0.17.2 h1:lwFfiS4sv5DvOrsYDsYq4N7UU8ghXiYtPJ+VcQnC3Xg=
github.com/go-openapi/validate v0.17.2/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/go-sql-driver/mysql v0.0.0-20171007150158-ee359f95877b h1:/CMGgAYard7jx9+bI7tUIqafFDR7Pv2BRu2Tb5dDaqM=
github.com/go-sql-driver/mysql v0.0.0-20171007150158-ee359f95877b/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-swagger/go-swagger v0.17.2 h1:eizwRyO8THHMA4kXyM5Z1UTPslZGE8VsfJC0jJqsRI8=
github.com/go-swagger/go-swagger v0.17.2/go.mod h1:fOcXeMI1KPNv3uk4u7cR4VSyq0NyrYx4SS1/ajuTWDg=
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25 h1:jUX9yw6+iKrs/WuysV2M6ap/ObK/07SE/a7I2uxitwM=
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25/go.mod h1:M+P3wv0K2C+ynucGDEqJCeOTc+6DcAtiiqU8GrCksXY=
github.com/go-xorm/core v0.5.8 h1:vQ0ghlVGnlnFmm4SpHY+xNnPlH810paMcw+Hwz9BCqE=
@@ -26,12 +72,26 @@ github.com/go-xorm/xorm-redis-cache v0.0.0-20180727005610-859b313566b2 h1:57QbyU
github.com/go-xorm/xorm-redis-cache v0.0.0-20180727005610-859b313566b2/go.mod h1:xxK9FGkFXrau9/vGdDYSOyQfSgKXBV7iHXpQfNuv6B0=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY=
github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/karalabe/xgo v0.0.0-20181007145344-72da7d1d3970 h1:0+1ZURVRim6FxA/jhWhJklsgoWc69q1sxlIu2Ztnhy0=
github.com/karalabe/xgo v0.0.0-20181007145344-72da7d1d3970/go.mod h1:iYGcTYIPUvEWhFo6aKUuLchs+AV4ssYdyuBbQJZGcBk=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo v3.1.0+incompatible h1:kb0CCZ0boaiGsZOqR9E9+GDpQEoIaKClVqqo0+/hzbM=
github.com/labstack/echo v3.1.0+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.0.0-20170925052817-57409ada9da0 h1:kcJPx2Ug9owxOsVfuXPCludLaIudyI57YQd6ocyrO4o=
@@ -40,6 +100,8 @@ github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.0.0-20170816031813-ad5389df28cd h1:eYiiP5pgdf+n78BU5JFWt7yI2bpxW31L/R5Rrk8vLgs=
github.com/mattn/go-colorable v0.0.0-20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.0-20170925054904-a5cdd64afdee h1:tUyoJR5V1TdXnTh9v8c1YAHvDdut2+zkuyUX3gAY/wI=
@@ -50,8 +112,11 @@ github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 h1:J1QZwDXgZ4dJD2s19iqR9+U00OWM2kDzbf1O/fmvCWg=
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -66,8 +131,10 @@ github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.2.0 h1:M4Rzxlu+RgU4pyBRKhKaVN1VeYOm8h2jgyXnAseDgCc=
github.com/spf13/viper v1.2.0/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
github.com/stretchr/testify v0.0.0-20171231124224-87b1dfb5b2fa h1:ws/U/9eA/uVBX3BckIHVlYLtQLuWodrnpPBuL8Q0N1E=
github.com/stretchr/testify v0.0.0-20171231124224-87b1dfb5b2fa/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/toqueteos/webbrowser v1.1.0 h1:Prj1okiysRgHPoe3B1bOIVxcv+UuSt525BDQmR5W0x0=
github.com/toqueteos/webbrowser v1.1.0/go.mod h1:Hqqqmzj8AHn+VlZyVjaRWY20i25hoOZGAABCcg2el4A=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
@@ -76,12 +143,22 @@ github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081 h1:QJP9sxq2/KbTxFnGduVryxJOt6r/UVGyom3tLaqu7tc=
golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/testfixtures.v2 v2.4.5 h1:mnfYPBNoJnis+4crs6UzC4lv4GjTVoLXE9B/tW802q0=
gopkg.in/testfixtures.v2 v2.4.5/go.mod h1:vyAq+MYCgNpR29qitQdLZhdbLFf4mR/2MFJRFoQZZ2M=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=

22
main.go
View File

@@ -1,8 +1,11 @@
package main
import (
"code.vikunja.io/api/models"
"code.vikunja.io/api/routes"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/routes"
"context"
"github.com/spf13/viper"
@@ -17,24 +20,27 @@ var Version = "0.1"
func main() {
// Init logging
models.InitLogger()
log.InitLogger()
// Init Config
err := models.InitConfig()
err := config.InitConfig()
if err != nil {
models.Log.Error(err.Error())
log.Log.Error(err.Error())
os.Exit(1)
}
// Set Engine
err = models.SetEngine()
if err != nil {
models.Log.Error(err.Error())
log.Log.Error(err.Error())
os.Exit(1)
}
// Start the mail daemon
mail.StartMailDaemon()
// Version notification
models.Log.Infof("Vikunja version %s", Version)
log.Log.Infof("Vikunja version %s", Version)
// Start the webserver
e := routes.NewEcho()
@@ -53,7 +59,7 @@ func main() {
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
models.Log.Infof("Sutting down...")
log.Log.Infof("Shutting down...")
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}

View File

@@ -1,80 +0,0 @@
package models
// TeamRight defines the rights teams can have for lists/namespaces
type TeamRight int
// define unknown team right
const (
TeamRightUnknown = -1
)
// Enumerate all the team rights
const (
// Can read lists in a Team
TeamRightRead TeamRight = iota
// Can write tasks in a Team like lists and todo tasks. Cannot create new lists.
TeamRightWrite
// Can manage a list/namespace, can do everything
TeamRightAdmin
)
func (r TeamRight) isValid() error {
if r != TeamRightAdmin && r != TeamRightRead && r != TeamRightWrite {
return ErrInvalidTeamRight{r}
}
return nil
}
// CanCreate checks if the user can create a new team
func (t *Team) CanCreate(user *User) bool {
// This is currently a dummy function, later on we could imagine global limits etc.
return true
}
// CanUpdate checks if the user can update a team
func (t *Team) CanUpdate(user *User) bool {
// Check if the current user is in the team and has admin rights in it
exists, err := x.Where("team_id = ?", t.ID).
And("user_id = ?", user.ID).
And("admin = ?", true).
Get(&TeamMember{})
if err != nil {
Log.Error("Error occurred during CanUpdate for Team: %s", err)
return false
}
return exists
}
// CanDelete checks if a user can delete a team
func (t *Team) CanDelete(user *User) bool {
return t.IsAdmin(user)
}
// IsAdmin returns true when the user is admin of a team
func (t *Team) IsAdmin(user *User) bool {
exists, err := x.Where("team_id = ?", t.ID).
And("user_id = ?", user.ID).
And("admin = ?", true).
Get(&TeamMember{})
if err != nil {
Log.Error("Error occurred during CanUpdate for Team: %s", err)
return false
}
return exists
}
// CanRead returns true if the user has read access to the team
func (t *Team) CanRead(user *User) bool {
// Check if the user is in the team
exists, err := x.Where("team_id = ?", t.ID).
And("user_id = ?", user.ID).
Get(&TeamMember{})
if err != nil {
Log.Error("Error occurred during CanUpdate for Team: %s", err)
return false
}
return exists
}

View File

@@ -1,9 +1,11 @@
package models
package config
import (
"crypto/rand"
"fmt"
"github.com/spf13/viper"
"os"
"path/filepath"
"strings"
)
@@ -17,8 +19,16 @@ func InitConfig() (err error) {
return err
}
// Service
viper.SetDefault("service.JWTSecret", random)
viper.SetDefault("service.interface", ":3456")
viper.SetDefault("service.frontendurl", "")
ex, err := os.Executable()
if err != nil {
panic(err)
}
exPath := filepath.Dir(ex)
viper.SetDefault("service.rootpath", exPath)
// Database
viper.SetDefault("database.type", "sqlite")
viper.SetDefault("database.host", "localhost")
@@ -34,6 +44,15 @@ func InitConfig() (err error) {
viper.SetDefault("cache.maxelementsize", 1000)
viper.SetDefault("cache.redishost", "localhost:6379")
viper.SetDefault("cache.redispassword", "")
// Mailer
viper.SetDefault("mailer.host", "")
viper.SetDefault("mailer.port", "587")
viper.SetDefault("mailer.user", "user")
viper.SetDefault("mailer.password", "")
viper.SetDefault("mailer.skiptlsverify", false)
viper.SetDefault("mailer.fromemail", "mail@vikunja")
viper.SetDefault("mailer.queuelength", 100)
viper.SetDefault("mailer.queuetimeout", 30)
// Init checking for environment variables
viper.SetEnvPrefix("vikunja")

View File

@@ -1,4 +1,4 @@
package models
package log
import (
"github.com/op/go-logging"

63
pkg/mail/mail.go Normal file
View File

@@ -0,0 +1,63 @@
package mail
import (
"code.vikunja.io/api/pkg/log"
"crypto/tls"
"github.com/spf13/viper"
"gopkg.in/gomail.v2"
"time"
)
// Queue is the mail queue
var Queue chan *gomail.Message
// StartMailDaemon starts the mail daemon
func StartMailDaemon() {
Queue = make(chan *gomail.Message, viper.GetInt("mailer.queuelength"))
if viper.GetString("mailer.host") == "" {
log.Log.Warning("Mailer seems to be not configured! Please see the config docs for more details.")
return
}
go func() {
d := gomail.NewDialer(viper.GetString("mailer.host"), viper.GetInt("mailer.port"), viper.GetString("mailer.username"), viper.GetString("mailer.password"))
d.TLSConfig = &tls.Config{InsecureSkipVerify: viper.GetBool("mailer.skiptlsverify")}
var s gomail.SendCloser
var err error
open := false
for {
select {
case m, ok := <-Queue:
if !ok {
return
}
if !open {
if s, err = d.Dial(); err != nil {
log.Log.Error("Error during connect to smtp server: %s", err)
}
open = true
}
if err := gomail.Send(s, m); err != nil {
log.Log.Error("Error when sending mail: %s", err)
}
// Close the connection to the SMTP server if no email was sent in
// the last 30 seconds.
case <-time.After(viper.GetDuration("mailer.queuetimeout") * time.Second):
if open {
if err := s.Close(); err != nil {
log.Log.Error("Error closing the mail server connection: %s\n", err)
}
log.Log.Infof("Closed connection to mailserver")
open = false
}
}
}
}()
}
// StopMailDaemon closes the mail queue channel, aka stops the daemon
func StopMailDaemon() {
close(Queue)
}

100
pkg/mail/send_mail.go Normal file
View File

@@ -0,0 +1,100 @@
package mail
import (
"bytes"
"code.vikunja.io/api/pkg/utils"
"github.com/labstack/gommon/log"
"github.com/spf13/viper"
"gopkg.in/gomail.v2"
"text/template"
)
// Opts holds infos for a mail
type Opts struct {
To string
Subject string
Message string
HTMLMessage string
ContentType ContentType
Boundary string
Headers []*header
}
// ContentType represents mail content types
type ContentType int
// Enumerate all the team rights
const (
ContentTypePlain ContentType = iota
ContentTypeHTML
ContentTypeMultipart
)
type header struct {
Field string
Content string
}
// SendMail puts a mail in the queue
func SendMail(opts *Opts) {
m := gomail.NewMessage()
m.SetHeader("From", viper.GetString("mailer.fromemail"))
m.SetHeader("To", opts.To)
m.SetHeader("Subject", opts.Subject)
for _, h := range opts.Headers {
m.SetHeader(h.Field, h.Content)
}
switch opts.ContentType {
case ContentTypePlain:
m.SetBody("text/plain", opts.Message)
case ContentTypeHTML:
m.SetBody("text/html", opts.Message)
case ContentTypeMultipart:
m.SetBody("text/plain", opts.Message)
m.AddAlternative("text/html", opts.HTMLMessage)
}
Queue <- m
}
// Template holds a pointer about a template
type Template struct {
Templates *template.Template
}
// SendMailWithTemplate parses a template and sends it via mail
func SendMailWithTemplate(to, subject, tpl string, data map[string]interface{}) {
var htmlContent bytes.Buffer
var plainContent bytes.Buffer
t := &Template{
Templates: template.Must(template.ParseGlob(viper.GetString("service.rootpath") + "/templates/mail/*.tmpl")),
}
boundary := "np" + utils.MakeRandomString(13)
data["Boundary"] = boundary
data["FrontendURL"] = viper.GetString("service.frontendurl")
if err := t.Templates.ExecuteTemplate(&htmlContent, tpl+".html.tmpl", data); err != nil {
log.Error(3, "Template: %v", err)
return
}
if err := t.Templates.ExecuteTemplate(&plainContent, tpl+".plain.tmpl", data); err != nil {
log.Error(3, "Template: %v", err)
return
}
opts := &Opts{
To: to,
Subject: subject,
Message: plainContent.String(),
HTMLMessage: htmlContent.String(),
ContentType: ContentTypeMultipart,
Boundary: boundary,
}
SendMail(opts)
}

View File

@@ -134,25 +134,112 @@ func (err ErrCouldNotGetUserID) HTTPError() HTTPError {
return HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeCouldNotGetUserID, Message: "Could not get user id."}
}
// ErrCannotDeleteLastUser represents a "ErrCannotDeleteLastUser" kind of error.
type ErrCannotDeleteLastUser struct{}
// ErrNoPasswordResetToken represents an error where no password reset token exists for that user
type ErrNoPasswordResetToken struct {
UserID int64
}
// IsErrCannotDeleteLastUser checks if an error is a ErrCannotDeleteLastUser.
func IsErrCannotDeleteLastUser(err error) bool {
_, ok := err.(ErrCannotDeleteLastUser)
func (err ErrNoPasswordResetToken) Error() string {
return fmt.Sprintf("No token to reset a password [UserID: %d]", err.UserID)
}
// ErrCodeNoPasswordResetToken holds the unique world-error code of this error
const ErrCodeNoPasswordResetToken = 1008
// HTTPError holds the http error description
func (err ErrNoPasswordResetToken) HTTPError() HTTPError {
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeNoPasswordResetToken, Message: "No token to reset a user's password provided."}
}
// ErrInvalidPasswordResetToken is an error where the password reset token is invalid
type ErrInvalidPasswordResetToken struct {
Token string
}
func (err ErrInvalidPasswordResetToken) Error() string {
return fmt.Sprintf("Invalid token to reset a password [Token: %s]", err.Token)
}
// ErrCodeInvalidPasswordResetToken holds the unique world-error code of this error
const ErrCodeInvalidPasswordResetToken = 1009
// HTTPError holds the http error description
func (err ErrInvalidPasswordResetToken) HTTPError() HTTPError {
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeInvalidPasswordResetToken, Message: "Invalid token to reset a user's password."}
}
// IsErrInvalidPasswordResetToken checks if an error is a ErrInvalidPasswordResetToken.
func IsErrInvalidPasswordResetToken(err error) bool {
_, ok := err.(ErrInvalidPasswordResetToken)
return ok
}
func (err ErrCannotDeleteLastUser) Error() string {
return fmt.Sprintf("Cannot delete last user")
// ErrInvalidEmailConfirmToken is an error where the email confirm token is invalid
type ErrInvalidEmailConfirmToken struct {
Token string
}
// ErrCodeCannotDeleteLastUser holds the unique world-error code of this error
const ErrCodeCannotDeleteLastUser = 1007
func (err ErrInvalidEmailConfirmToken) Error() string {
return fmt.Sprintf("Invalid email confirm token [Token: %s]", err.Token)
}
// ErrCodeInvalidEmailConfirmToken holds the unique world-error code of this error
const ErrCodeInvalidEmailConfirmToken = 1010
// HTTPError holds the http error description
func (err ErrCannotDeleteLastUser) HTTPError() HTTPError {
return HTTPError{HTTPCode: http.StatusConflict, Code: ErrCodeCannotDeleteLastUser, Message: "Cannot delete the last user on the server."}
func (err ErrInvalidEmailConfirmToken) HTTPError() HTTPError {
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeInvalidEmailConfirmToken, Message: "Invalid email confirm token."}
}
// IsErrInvalidEmailConfirmToken checks if an error is a ErrInvalidEmailConfirmToken.
func IsErrInvalidEmailConfirmToken(err error) bool {
_, ok := err.(ErrInvalidEmailConfirmToken)
return ok
}
// ErrWrongUsernameOrPassword is an error where the email was not confirmed
type ErrWrongUsernameOrPassword struct {
}
func (err ErrWrongUsernameOrPassword) Error() string {
return fmt.Sprintf("Wrong username or password")
}
// ErrCodeWrongUsernameOrPassword holds the unique world-error code of this error
const ErrCodeWrongUsernameOrPassword = 1011
// HTTPError holds the http error description
func (err ErrWrongUsernameOrPassword) HTTPError() HTTPError {
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeWrongUsernameOrPassword, Message: "Wrong username or password."}
}
// IsErrWrongUsernameOrPassword checks if an error is a IsErrEmailNotConfirmed.
func IsErrWrongUsernameOrPassword(err error) bool {
_, ok := err.(ErrWrongUsernameOrPassword)
return ok
}
// ErrEmailNotConfirmed is an error where the email was not confirmed
type ErrEmailNotConfirmed struct {
UserID int64
}
func (err ErrEmailNotConfirmed) Error() string {
return fmt.Sprintf("Email is not confirmed [UserID: %d]", err.UserID)
}
// ErrCodeEmailNotConfirmed holds the unique world-error code of this error
const ErrCodeEmailNotConfirmed = 1012
// HTTPError holds the http error description
func (err ErrEmailNotConfirmed) HTTPError() HTTPError {
return HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeEmailNotConfirmed, Message: "Please confirm your email address."}
}
// IsErrEmailNotConfirmed checks if an error is a IsErrEmailNotConfirmed.
func IsErrEmailNotConfirmed(err error) bool {
_, ok := err.(ErrEmailNotConfirmed)
return ok
}
// ===================

View File

@@ -25,9 +25,9 @@ func GetListsByNamespaceID(nID int64) (lists []*List, err error) {
}
// ReadAll gets all lists a user has access to
func (l *List) ReadAll(user *User) (interface{}, error) {
func (l *List) ReadAll(u *User) (interface{}, error) {
lists := []*List{}
fullUser, err := GetUserByID(user.ID)
fullUser, err := GetUserByID(u.ID)
if err != nil {
return lists, err
}
@@ -53,7 +53,7 @@ func (l *List) ReadAll(user *User) (interface{}, error) {
Find(&lists)
// Add more list details
addListDetails(lists)
AddListDetails(lists)
return lists, err
}
@@ -98,8 +98,8 @@ func (l *List) GetSimpleByID() (err error) {
return
}
// Adds owner user objects and list tasks to all lists in the slice
func addListDetails(lists []*List) (err error) {
// AddListDetails adds owner user objects and list tasks to all lists in the slice
func AddListDetails(lists []*List) (err error) {
var listIDs []int64
var ownerIDs []int64
for _, l := range lists {
@@ -108,8 +108,8 @@ func addListDetails(lists []*List) (err error) {
}
// Get all tasks
tasks := []*ListTask{}
err = x.In("list_id", listIDs).Find(&tasks)
ts := []*ListTask{}
err = x.In("list_id", listIDs).Find(&ts)
if err != nil {
return
}
@@ -132,7 +132,7 @@ func addListDetails(lists []*List) (err error) {
}
// Tasks
for _, task := range tasks {
for _, task := range ts {
if task.ListID == list.ID {
lists[in].Tasks = append(lists[in].Tasks, task)
}

View File

@@ -44,13 +44,13 @@ func (l *List) Update() (err error) {
// Create implements the create method of CRUDable
func (l *List) Create(doer *User) (err error) {
// Check rights
user, err := GetUserByID(doer.ID)
u, err := GetUserByID(doer.ID)
if err != nil {
return
}
l.OwnerID = user.ID
l.Owner.ID = user.ID
l.OwnerID = u.ID
l.Owner.ID = u.ID
l.ID = 0 // Otherwise only the first time a new list would be created
return CreateOrUpdateList(l)

View File

@@ -16,11 +16,11 @@ func TestList_ReadAll(t *testing.T) {
assert.Equal(t, len(lists), 2)
// Get all lists our user has access to
user, err := GetUserByID(1)
u, err := GetUserByID(1)
assert.NoError(t, err)
lists2 := List{}
lists3, err := lists2.ReadAll(&user)
lists3, err := lists2.ReadAll(&u)
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice)
s := reflect.ValueOf(lists3)

View File

@@ -1,18 +1,22 @@
package models
import (
"code.vikunja.io/api/pkg/log"
)
// IsAdmin returns whether the user has admin rights on the list or not
func (l *List) IsAdmin(user *User) bool {
func (l *List) IsAdmin(u *User) bool {
// Owners are always admins
if l.Owner.ID == user.ID {
if l.Owner.ID == u.ID {
return true
}
// Check individual rights
if l.checkListUserRight(user, UserRightAdmin) {
if l.checkListUserRight(u, UserRightAdmin) {
return true
}
return l.checkListTeamRight(user, TeamRightAdmin)
return l.checkListTeamRight(u, TeamRightAdmin)
}
// CanWrite return whether the user can write on that list or not
@@ -48,7 +52,7 @@ func (l *List) CanRead(user *User) bool {
// CanDelete checks if the user can delete a list
func (l *List) CanDelete(doer *User) bool {
if err := l.GetSimpleByID(); err != nil {
Log.Error("Error occurred during CanDelete for List: %s", err)
log.Log.Error("Error occurred during CanDelete for List: %s", err)
return false
}
return l.IsAdmin(doer)
@@ -57,7 +61,7 @@ func (l *List) CanDelete(doer *User) bool {
// CanUpdate checks if the user can update a list
func (l *List) CanUpdate(doer *User) bool {
if err := l.GetSimpleByID(); err != nil {
Log.Error("Error occurred during CanUpdate for List: %s", err)
log.Log.Error("Error occurred during CanUpdate for List: %s", err)
return false
}
return l.CanWrite(doer)
@@ -82,7 +86,7 @@ func (l *List) checkListTeamRight(user *User, r TeamRight) bool {
user.ID, r, user.ID, r, l.ID).
Exist(&List{})
if err != nil {
Log.Error("Error occurred during checkListTeamRight for List: %s", err)
log.Log.Error("Error occurred during checkListTeamRight for List: %s", err)
return false
}
@@ -103,7 +107,7 @@ func (l *List) checkListUserRight(user *User, r UserRight) bool {
user.ID, r, user.ID, r, user.ID, l.ID).
Exist(&List{})
if err != nil {
Log.Error("Error occurred during checkListUserRight for List: %s", err)
log.Log.Error("Error occurred during checkListUserRight for List: %s", err)
return false
}

View File

@@ -59,9 +59,9 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
}
for in, task := range tasks {
for _, user := range users {
if task.CreatedByID == user.ID {
tasks[in].CreatedBy = user
for _, u := range users {
if task.CreatedByID == u.ID {
tasks[in].CreatedBy = u
break
}
}
@@ -88,11 +88,11 @@ func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
return ListTask{}, ErrListTaskDoesNotExist{listTaskID}
}
user, err := GetUserByID(listTask.CreatedByID)
u, err := GetUserByID(listTask.CreatedByID)
if err != nil {
return
}
listTask.CreatedBy = user
listTask.CreatedBy = u
return
}

View File

@@ -19,13 +19,13 @@ func (i *ListTask) Create(doer *User) (err error) {
return
}
user, err := GetUserByID(doer.ID)
u, err := GetUserByID(doer.ID)
if err != nil {
return err
}
i.CreatedByID = user.ID
i.CreatedBy = user
i.CreatedByID = u.ID
i.CreatedBy = u
_, err = x.Cols("text", "description", "done", "due_date_unix", "reminder_unix", "created_by_id", "list_id", "created", "updated").Insert(i)
return err
}
@@ -39,8 +39,8 @@ func (i *ListTask) Update() (err error) {
}
// For whatever reason, xorm dont detect if done is updated, so we need to update this every time by hand
// Which is why we merge the actual task struct with the one we got from the user.
// The user struct ovverrides values in the actual one.
// Which is why we merge the actual task struct with the one we got from the
// The user struct overrides values in the actual one.
if err := mergo.Merge(&ot, i, mergo.WithOverride); err != nil {
return err
}

View File

@@ -1,18 +1,22 @@
package models
import (
"code.vikunja.io/api/pkg/log"
)
// CanDelete checks if the user can delete an task
func (i *ListTask) CanDelete(doer *User) bool {
// Get the task
lI, err := GetListTaskByID(i.ID)
if err != nil {
Log.Error("Error occurred during CanDelete for ListTask: %s", err)
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
return false
}
// A user can delete an task if he has write acces to its list
list := &List{ID: lI.ListID}
list.ReadOne()
return list.CanWrite(doer)
l := &List{ID: lI.ListID}
l.ReadOne()
return l.CanWrite(doer)
}
// CanUpdate determines if a user has the right to update a list task
@@ -20,20 +24,20 @@ func (i *ListTask) CanUpdate(doer *User) bool {
// Get the task
lI, err := GetListTaskByID(i.ID)
if err != nil {
Log.Error("Error occurred during CanDelete for ListTask: %s", err)
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
return false
}
// A user can update an task if he has write acces to its list
list := &List{ID: lI.ListID}
list.ReadOne()
return list.CanWrite(doer)
l := &List{ID: lI.ListID}
l.ReadOne()
return l.CanWrite(doer)
}
// CanCreate determines if a user has the right to create a list task
func (i *ListTask) CanCreate(doer *User) bool {
// A user can create an task if he has write acces to its list
list := &List{ID: i.ListID}
list.ReadOne()
return list.CanWrite(doer)
l := &List{ID: i.ListID}
l.ReadOne()
return l.CanWrite(doer)
}

View File

@@ -1,7 +1,7 @@
package models
// Create creates a new list <-> user relation
func (ul *ListUser) Create(user *User) (err error) {
func (ul *ListUser) Create(u *User) (err error) {
// Check if the right is valid
if err := ul.Right.isValid(); err != nil {

View File

@@ -1,13 +1,13 @@
package models
// ReadAll gets all users who have access to a list
func (ul *ListUser) ReadAll(user *User) (interface{}, error) {
func (ul *ListUser) ReadAll(u *User) (interface{}, error) {
// Check if the user has access to the list
l := &List{ID: ul.ListID}
if err := l.GetSimpleByID(); err != nil {
return nil, err
}
if !l.CanRead(user) {
if !l.CanRead(u) {
return nil, ErrNeedToHaveListReadAccess{}
}

View File

@@ -1,37 +1,15 @@
package models
// UserRight defines the rights users can have for lists/namespaces
type UserRight int
// define unknown user right
const (
UserRightUnknown = -1
import (
"code.vikunja.io/api/pkg/log"
)
// Enumerate all the user rights
const (
// Can read lists in a User
UserRightRead UserRight = iota
// Can write tasks in a User like lists and todo tasks. Cannot create new lists.
UserRightWrite
// Can manage a list/namespace, can do everything
UserRightAdmin
)
func (r UserRight) isValid() error {
if r != UserRightAdmin && r != UserRightRead && r != UserRightWrite {
return ErrInvalidUserRight{r}
}
return nil
}
// CanCreate checks if the user can create a new user <-> list relation
func (lu *ListUser) CanCreate(doer *User) bool {
// Get the list and check if the user has write access on it
l := List{ID: lu.ListID}
if err := l.GetSimpleByID(); err != nil {
Log.Error("Error occurred during CanCreate for ListUser: %s", err)
log.Log.Error("Error occurred during CanCreate for ListUser: %s", err)
return false
}
return l.CanWrite(doer)
@@ -42,7 +20,7 @@ func (lu *ListUser) CanDelete(doer *User) bool {
// Get the list and check if the user has write access on it
l := List{ID: lu.ListID}
if err := l.GetSimpleByID(); err != nil {
Log.Error("Error occurred during CanDelete for ListUser: %s", err)
log.Log.Error("Error occurred during CanDelete for ListUser: %s", err)
return false
}
return l.CanWrite(doer)
@@ -53,7 +31,7 @@ func (lu *ListUser) CanUpdate(doer *User) bool {
// Get the list and check if the user has write access on it
l := List{ID: lu.ListID}
if err := l.GetSimpleByID(); err != nil {
Log.Error("Error occurred during CanUpdate for ListUser: %s", err)
log.Log.Error("Error occurred during CanUpdate for ListUser: %s", err)
return false
}
return l.CanWrite(doer)

View File

@@ -109,7 +109,7 @@ func (n *Namespace) ReadAll(doer *User) (interface{}, error) {
}
// More details for the lists
addListDetails(lists)
AddListDetails(lists)
// Put objects in our namespace list
for i, n := range all {

View File

@@ -21,8 +21,8 @@ func (n *Namespace) Delete() (err error) {
// We need to do that for here because we need the list ids to delete two times:
// 1) to delete the lists itself
// 2) to delete the list tasks
for _, list := range lists {
listIDs = append(listIDs, list.ID)
for _, l := range lists {
listIDs = append(listIDs, l.ID)
}
// Delete tasks

View File

@@ -1,106 +1,110 @@
package models
import (
"code.vikunja.io/api/pkg/log"
)
// IsAdmin returns true or false if the user is admin on that namespace or not
func (n *Namespace) IsAdmin(user *User) bool {
func (n *Namespace) IsAdmin(u *User) bool {
// Owners always have admin rights
if user.ID == n.Owner.ID {
if u.ID == n.Owner.ID {
return true
}
// Check user rights
if n.checkUserRights(user, UserRightAdmin) {
if n.checkUserRights(u, UserRightAdmin) {
return true
}
// Check if that user is in a team which has admin rights to that namespace
return n.checkTeamRights(user, TeamRightAdmin)
return n.checkTeamRights(u, TeamRightAdmin)
}
// CanWrite checks if a user has write access to a namespace
func (n *Namespace) CanWrite(user *User) bool {
func (n *Namespace) CanWrite(u *User) bool {
// Admins always have write access
if n.IsAdmin(user) {
if n.IsAdmin(u) {
return true
}
// Check user rights
if n.checkUserRights(user, UserRightWrite) {
if n.checkUserRights(u, UserRightWrite) {
return true
}
// Check if that user is in a team which has write rights to that namespace
return n.checkTeamRights(user, TeamRightWrite)
return n.checkTeamRights(u, TeamRightWrite)
}
// CanRead checks if a user has read access to that namespace
func (n *Namespace) CanRead(user *User) bool {
func (n *Namespace) CanRead(u *User) bool {
// Admins always have read access
if n.IsAdmin(user) {
if n.IsAdmin(u) {
return true
}
// Check user rights
if n.checkUserRights(user, UserRightRead) {
if n.checkUserRights(u, UserRightRead) {
return true
}
// Check if the user is in a team which has access to the namespace
return n.checkTeamRights(user, TeamRightRead)
return n.checkTeamRights(u, TeamRightRead)
}
// CanUpdate checks if the user can update the namespace
func (n *Namespace) CanUpdate(user *User) bool {
func (n *Namespace) CanUpdate(u *User) bool {
nn, err := GetNamespaceByID(n.ID)
if err != nil {
Log.Error("Error occurred during CanUpdate for Namespace: %s", err)
log.Log.Error("Error occurred during CanUpdate for Namespace: %s", err)
return false
}
return nn.IsAdmin(user)
return nn.IsAdmin(u)
}
// CanDelete checks if the user can delete a namespace
func (n *Namespace) CanDelete(user *User) bool {
func (n *Namespace) CanDelete(u *User) bool {
nn, err := GetNamespaceByID(n.ID)
if err != nil {
Log.Error("Error occurred during CanDelete for Namespace: %s", err)
log.Log.Error("Error occurred during CanDelete for Namespace: %s", err)
return false
}
return nn.IsAdmin(user)
return nn.IsAdmin(u)
}
// CanCreate checks if the user can create a new namespace
func (n *Namespace) CanCreate(user *User) bool {
func (n *Namespace) CanCreate(u *User) bool {
// This is currently a dummy function, later on we could imagine global limits etc.
return true
}
func (n *Namespace) checkTeamRights(user *User, r TeamRight) bool {
func (n *Namespace) checkTeamRights(u *User, r TeamRight) bool {
exists, err := x.Select("namespaces.*").
Table("namespaces").
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
Where("namespaces.id = ? "+
"AND (team_members.user_id = ? AND team_namespaces.right = ?) "+
"OR namespaces.owner_id = ? ", n.ID, user.ID, r, user.ID).
"OR namespaces.owner_id = ? ", n.ID, u.ID, r, u.ID).
Get(&Namespace{})
if err != nil {
Log.Error("Error occurred during checkTeamRights for Namespace: %s, TeamRight: %d", err, r)
log.Log.Error("Error occurred during checkTeamRights for Namespace: %s, TeamRight: %d", err, r)
return false
}
return exists
}
func (n *Namespace) checkUserRights(user *User, r UserRight) bool {
func (n *Namespace) checkUserRights(u *User, r UserRight) bool {
exists, err := x.Select("namespaces.*").
Table("namespaces").
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
Where("namespaces.id = ? AND ("+
"namespaces.owner_id = ? "+
"OR (users_namespace.user_id = ? AND users_namespace.right = ?))", n.ID, user.ID, user.ID, r).
"OR (users_namespace.user_id = ? AND users_namespace.right = ?))", n.ID, u.ID, u.ID, r).
Get(&Namespace{})
if err != nil {
Log.Error("Error occurred during checkUserRights for Namespace: %s, UserRight: %d", err, r)
log.Log.Error("Error occurred during checkUserRights for Namespace: %s, UserRight: %d", err, r)
return false
}

View File

@@ -1,7 +1,7 @@
package models
// Create creates a new namespace <-> user relation
func (un *NamespaceUser) Create(user *User) (err error) {
func (un *NamespaceUser) Create(u *User) (err error) {
// Reset the id
un.ID = 0

View File

@@ -1,13 +1,13 @@
package models
// ReadAll gets all users who have access to a namespace
func (un *NamespaceUser) ReadAll(user *User) (interface{}, error) {
func (un *NamespaceUser) ReadAll(u *User) (interface{}, error) {
// Check if the user has access to the namespace
l, err := GetNamespaceByID(un.NamespaceID)
if err != nil {
return nil, err
}
if !l.CanRead(user) {
if !l.CanRead(u) {
return nil, ErrNeedToHaveNamespaceReadAccess{}
}

View File

@@ -1,11 +1,15 @@
package models
import (
"code.vikunja.io/api/pkg/log"
)
// CanCreate checks if the user can create a new user <-> namespace relation
func (nu *NamespaceUser) CanCreate(doer *User) bool {
// Get the namespace and check if the user has write access on it
n, err := GetNamespaceByID(nu.NamespaceID)
if err != nil {
Log.Error("Error occurred during CanCreate for NamespaceUser: %s", err)
log.Log.Error("Error occurred during CanCreate for NamespaceUser: %s", err)
return false
}
return n.CanWrite(doer)
@@ -16,7 +20,7 @@ func (nu *NamespaceUser) CanDelete(doer *User) bool {
// Get the namespace and check if the user has write access on it
n, err := GetNamespaceByID(nu.NamespaceID)
if err != nil {
Log.Error("Error occurred during CanCreate for NamespaceUser: %s", err)
log.Log.Error("Error occurred during CanDelete for NamespaceUser: %s", err)
return false
}
return n.CanWrite(doer)
@@ -27,7 +31,7 @@ func (nu *NamespaceUser) CanUpdate(doer *User) bool {
// Get the namespace and check if the user has write access on it
n, err := GetNamespaceByID(nu.NamespaceID)
if err != nil {
Log.Error("Error occurred during CanCreate for NamespaceUser: %s", err)
log.Log.Error("Error occurred during CanUpdate for NamespaceUser: %s", err)
return false
}
return n.CanWrite(doer)

View File

@@ -1,14 +1,14 @@
package models
// ReadAll implements the method to read all teams of a list
func (tl *TeamList) ReadAll(user *User) (interface{}, error) {
func (tl *TeamList) ReadAll(u *User) (interface{}, error) {
// Check if the user can read the namespace
l := &List{ID: tl.ListID}
if err := l.GetSimpleByID(); err != nil {
return nil, err
}
if !l.CanRead(user) {
return nil, ErrNeedToHaveListReadAccess{ListID: tl.ListID, UserID: user.ID}
if !l.CanRead(u) {
return nil, ErrNeedToHaveListReadAccess{ListID: tl.ListID, UserID: u.ID}
}
// Get the teams

View File

@@ -1,20 +1,24 @@
package models
import (
"code.vikunja.io/api/pkg/log"
)
// CanCreate checks if the user can create a team <-> list relation
func (tl *TeamList) CanCreate(user *User) bool {
func (tl *TeamList) CanCreate(u *User) bool {
l := List{ID: tl.ListID}
if err := l.GetSimpleByID(); err != nil {
Log.Error("Error occurred during CanCreate for TeamList: %s", err)
log.Log.Error("Error occurred during CanCreate for TeamList: %s", err)
return false
}
return l.IsAdmin(user)
return l.IsAdmin(u)
}
// CanDelete checks if the user can delete a team <-> list relation
func (tl *TeamList) CanDelete(user *User) bool {
l := List{ID: tl.ListID}
if err := l.GetSimpleByID(); err != nil {
Log.Error("Error occurred during CanDelete for TeamList: %s", err)
log.Log.Error("Error occurred during CanDelete for TeamList: %s", err)
return false
}
return l.IsAdmin(user)
@@ -24,7 +28,7 @@ func (tl *TeamList) CanDelete(user *User) bool {
func (tl *TeamList) CanUpdate(user *User) bool {
l := List{ID: tl.ListID}
if err := l.GetSimpleByID(); err != nil {
Log.Error("Error occurred during CanUpdate for TeamList: %s", err)
log.Log.Error("Error occurred during CanUpdate for TeamList: %s", err)
return false
}
return l.IsAdmin(user)

View File

@@ -15,67 +15,67 @@ func TestTeamList(t *testing.T) {
}
// Dummyuser
user, err := GetUserByID(1)
u, err := GetUserByID(1)
assert.NoError(t, err)
// Check normal creation
assert.True(t, tl.CanCreate(&user))
err = tl.Create(&user)
assert.True(t, tl.CanCreate(&u))
err = tl.Create(&u)
assert.NoError(t, err)
// Check again
err = tl.Create(&user)
err = tl.Create(&u)
assert.Error(t, err)
assert.True(t, IsErrTeamAlreadyHasAccess(err))
// Check with wrong rights
tl2 := tl
tl2.Right = TeamRightUnknown
err = tl2.Create(&user)
err = tl2.Create(&u)
assert.Error(t, err)
assert.True(t, IsErrInvalidTeamRight(err))
// Check with inexistant team
tl3 := tl
tl3.TeamID = 3253
err = tl3.Create(&user)
err = tl3.Create(&u)
assert.Error(t, err)
assert.True(t, IsErrTeamDoesNotExist(err))
// Check with inexistant list
tl4 := tl
tl4.ListID = 3252
err = tl4.Create(&user)
err = tl4.Create(&u)
assert.Error(t, err)
assert.True(t, IsErrListDoesNotExist(err))
// Test Read all
teams, err := tl.ReadAll(&user)
teams, err := tl.ReadAll(&u)
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
s := reflect.ValueOf(teams)
assert.Equal(t, s.Len(), 1)
// Test Read all for nonexistant list
_, err = tl4.ReadAll(&user)
_, err = tl4.ReadAll(&u)
assert.Error(t, err)
assert.True(t, IsErrListDoesNotExist(err))
// Test Read all for a list where the user is owner of the namespace this list belongs to
tl5 := tl
tl5.ListID = 2
_, err = tl5.ReadAll(&user)
_, err = tl5.ReadAll(&u)
assert.NoError(t, err)
// Test read all for a list where the user not has access
tl6 := tl
tl6.ListID = 3
_, err = tl6.ReadAll(&user)
_, err = tl6.ReadAll(&u)
assert.Error(t, err)
assert.True(t, IsErrNeedToHaveListReadAccess(err))
// Delete
assert.True(t, tl.CanDelete(&user))
assert.True(t, tl.CanDelete(&u))
err = tl.Delete()
assert.NoError(t, err)

View File

@@ -1,22 +1,26 @@
package models
import (
"code.vikunja.io/api/pkg/log"
)
// CanCreate checks if the user can add a new tem member
func (tm *TeamMember) CanCreate(user *User) bool {
return tm.IsAdmin(user)
func (tm *TeamMember) CanCreate(u *User) bool {
return tm.IsAdmin(u)
}
// CanDelete checks if the user can delete a new team member
func (tm *TeamMember) CanDelete(user *User) bool {
return tm.IsAdmin(user)
func (tm *TeamMember) CanDelete(u *User) bool {
return tm.IsAdmin(u)
}
// IsAdmin checks if the user is team admin
func (tm *TeamMember) IsAdmin(user *User) bool {
func (tm *TeamMember) IsAdmin(u *User) bool {
// A user can add a member to a team if he is admin of that team
exists, err := x.Where("user_id = ? AND team_id = ? AND admin = ?", user.ID, tm.TeamID, true).
exists, err := x.Where("user_id = ? AND team_id = ? AND admin = ?", u.ID, tm.TeamID, true).
Get(&TeamMember{})
if err != nil {
Log.Error("Error occurred during IsAdmin for TeamMember: %s", err)
log.Log.Error("Error occurred during IsAdmin for TeamMember: %s", err)
return false
}
return exists

View File

@@ -1,10 +1,14 @@
package models
import (
"code.vikunja.io/api/pkg/log"
)
// CanCreate checks if one can create a new team <-> namespace relation
func (tn *TeamNamespace) CanCreate(user *User) bool {
n, err := GetNamespaceByID(tn.NamespaceID)
if err != nil {
Log.Error("Error occurred during CanCreate for TeamNamespace: %s", err)
log.Log.Error("Error occurred during CanCreate for TeamNamespace: %s", err)
return false
}
return n.IsAdmin(user)
@@ -14,17 +18,17 @@ func (tn *TeamNamespace) CanCreate(user *User) bool {
func (tn *TeamNamespace) CanDelete(user *User) bool {
n, err := GetNamespaceByID(tn.NamespaceID)
if err != nil {
Log.Error("Error occurred during CanDelete for TeamNamespace: %s", err)
log.Log.Error("Error occurred during CanDelete for TeamNamespace: %s", err)
return false
}
return n.IsAdmin(user)
}
// CanUpdate checks if a user can update a team from a namespace. Only namespace admins can do that.
// CanUpdate checks if a user can update a team from a Only namespace admins can do that.
func (tn *TeamNamespace) CanUpdate(user *User) bool {
n, err := GetNamespaceByID(tn.NamespaceID)
if err != nil {
Log.Error("Error occurred during CanUpdate for TeamNamespace: %s", err)
log.Log.Error("Error occurred during CanUpdate for TeamNamespace: %s", err)
return false
}
return n.IsAdmin(user)

27
pkg/models/team_right.go Normal file
View File

@@ -0,0 +1,27 @@
package models
// TeamRight defines the rights teams can have for lists/namespaces
type TeamRight int
// define unknown team right
const (
TeamRightUnknown = -1
)
// Enumerate all the team rights
const (
// Can read lists in a Team
TeamRightRead TeamRight = iota
// Can write tasks in a Team like lists and todo tasks. Cannot create new lists.
TeamRightWrite
// Can manage a list/namespace, can do everything
TeamRightAdmin
)
func (r TeamRight) isValid() error {
if r != TeamRightAdmin && r != TeamRightRead && r != TeamRightWrite {
return ErrInvalidTeamRight{r}
}
return nil
}

View File

@@ -0,0 +1,58 @@
package models
import (
"code.vikunja.io/api/pkg/log"
)
// CanCreate checks if the user can create a new team
func (t *Team) CanCreate(u *User) bool {
// This is currently a dummy function, later on we could imagine global limits etc.
return true
}
// CanUpdate checks if the user can update a team
func (t *Team) CanUpdate(u *User) bool {
// Check if the current user is in the team and has admin rights in it
exists, err := x.Where("team_id = ?", t.ID).
And("user_id = ?", u.ID).
And("admin = ?", true).
Get(&TeamMember{})
if err != nil {
log.Log.Error("Error occurred during CanUpdate for Team: %s", err)
return false
}
return exists
}
// CanDelete checks if a user can delete a team
func (t *Team) CanDelete(u *User) bool {
return t.IsAdmin(u)
}
// IsAdmin returns true when the user is admin of a team
func (t *Team) IsAdmin(u *User) bool {
exists, err := x.Where("team_id = ?", t.ID).
And("user_id = ?", u.ID).
And("admin = ?", true).
Get(&TeamMember{})
if err != nil {
log.Log.Error("Error occurred during CanUpdate for Team: %s", err)
return false
}
return exists
}
// CanRead returns true if the user has read access to the team
func (t *Team) CanRead(user *User) bool {
// Check if the user is in the team
exists, err := x.Where("team_id = ?", t.ID).
And("user_id = ?", user.ID).
Get(&TeamMember{})
if err != nil {
log.Log.Error("Error occurred during CanUpdate for Team: %s", err)
return false
}
return exists
}

View File

@@ -1,6 +1,7 @@
package models
import (
"code.vikunja.io/api/pkg/mail"
"fmt"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
@@ -10,6 +11,10 @@ import (
"testing"
)
// IsTesting is set to true when we're running tests.
// We don't have a good solution to test email sending yet, so we disable email sending when testing
var IsTesting bool
// MainTest creates the test engine
func MainTest(m *testing.M, pathToRoot string) {
var err error
@@ -19,6 +24,11 @@ func MainTest(m *testing.M, pathToRoot string) {
os.Exit(1)
}
IsTesting = true
// Start the pseudo mail queue
mail.StartMailDaemon()
// Create test database
PrepareTestDatabase()

View File

@@ -18,8 +18,13 @@ type User struct {
Username string `xorm:"varchar(250) not null unique" json:"username"`
Password string `xorm:"varchar(250) not null" json:"-"`
Email string `xorm:"varchar(250)" json:"email"`
Created int64 `xorm:"created" json:"-"`
Updated int64 `xorm:"updated" json:"-"`
IsActive bool `json:"-"`
PasswordResetToken string `xorm:"varchar(450)" json:"-"`
EmailConfirmToken string `xorm:"varchar(450)" json:"-"`
Created int64 `xorm:"created" json:"-"`
Updated int64 `xorm:"updated" json:"-"`
}
// TableName returns the table name for users
@@ -61,7 +66,7 @@ func GetUser(user User) (userOut User, err error) {
exists, err := x.Get(&userOut)
if !exists {
return User{}, ErrUserDoesNotExist{}
return User{}, ErrUserDoesNotExist{UserID: user.ID}
}
return userOut, err
@@ -69,6 +74,10 @@ func GetUser(user User) (userOut User, err error) {
// CheckUserCredentials checks user credentials
func CheckUserCredentials(u *UserLogin) (User, error) {
// Check if we have any credentials
if u.Password == "" || u.Username == "" {
return User{}, ErrNoUsernamePassword{}
}
// Check if the user exists
user, err := GetUser(User{Username: u.Username})
@@ -76,10 +85,17 @@ func CheckUserCredentials(u *UserLogin) (User, error) {
return User{}, err
}
// User is invalid if it needs to verify its email address
if !user.IsActive {
return User{}, ErrEmailNotConfirmed{UserID: user.ID}
}
// Check the users password
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password))
if err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {
return User{}, ErrWrongUsernameOrPassword{}
}
return User{}, err
}

View File

@@ -1,6 +1,8 @@
package models
import (
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/utils"
"golang.org/x/crypto/bcrypt"
)
@@ -48,6 +50,12 @@ func CreateUser(user User) (newUser User, err error) {
return User{}, err
}
// Generate a confirm token
newUser.EmailConfirmToken = utils.MakeRandomString(400)
// The new user should not be activated until it confirms his mail address
newUser.IsActive = false
// Insert it
_, err = x.Insert(newUser)
if err != nil {
@@ -67,6 +75,18 @@ func CreateUser(user User) (newUser User, err error) {
return User{}, err
}
// Dont send a mail if we're testing
if IsTesting {
return newUserOut, err
}
// Send the user a mail with a link to confirm the mail
data := map[string]interface{}{
"User": newUserOut,
}
mail.SendMailWithTemplate(user.Email, newUserOut.Username+" + Vikunja = <3", "confirm-email", data)
return newUserOut, err
}

View File

@@ -7,18 +7,8 @@ func DeleteUserByID(id int64, doer *User) error {
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{})
_, err := x.Id(id).Delete(&User{})
if err != nil {
return err

View File

@@ -0,0 +1,31 @@
package models
// EmailConfirm holds the token to confirm a mail address
type EmailConfirm struct {
Token string `json:"token"`
}
// UserEmailConfirm handles the confirmation of an email address
func UserEmailConfirm(c *EmailConfirm) (err error) {
// Check if we have an email confirm token
if c.Token == "" {
return ErrInvalidEmailConfirmToken{}
}
// Check if the token is valid
user := User{}
has, err := x.Where("email_confirm_token = ?", c.Token).Get(&user)
if err != nil {
return
}
if !has {
return ErrInvalidEmailConfirmToken{Token: c.Token}
}
user.IsActive = true
user.EmailConfirmToken = ""
_, err = x.Where("id = ?", user.ID).Cols("is_active", "email_confirm_token").Update(&user)
return
}

View File

@@ -0,0 +1,94 @@
package models
import (
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/utils"
)
// PasswordReset holds the data to reset a password
type PasswordReset struct {
Token string `json:"token"`
NewPassword string `json:"new_password"`
}
// UserPasswordReset resets a users password
func UserPasswordReset(reset *PasswordReset) (err error) {
// Check if the password is not empty
if reset.NewPassword == "" {
return ErrNoUsernamePassword{}
}
// Check if we have a token
var user User
exists, err := x.Where("password_reset_token = ?", reset.Token).Get(&user)
if err != nil {
return
}
if !exists {
return ErrInvalidPasswordResetToken{Token: reset.Token}
}
// Hash the password
user.Password, err = hashPassword(reset.NewPassword)
if err != nil {
return
}
// Save it
_, err = x.Where("id = ?", user.ID).Update(&user)
if err != nil {
return
}
// Dont send a mail if we're testing
if IsTesting {
return
}
// Send a mail to the user to notify it his password was changed.
data := map[string]interface{}{
"User": user,
}
mail.SendMailWithTemplate(user.Email, "Your password on Vikunja was changed", "password-changed", data)
return
}
// PasswordTokenRequest defines the request format for password reset resqest
type PasswordTokenRequest struct {
Username string `json:"user_name"`
}
// RequestUserPasswordResetToken inserts a random token to reset a users password into the databsse
func RequestUserPasswordResetToken(tr *PasswordTokenRequest) (err error) {
// Check if the user exists
user, err := GetUser(User{Username: tr.Username})
if err != nil {
return
}
// Generate a token and save it
user.PasswordResetToken = utils.MakeRandomString(400)
// Save it
_, err = x.Where("id = ?", user.ID).Update(&user)
if err != nil {
return
}
// Dont send a mail if we're testing
if IsTesting {
return
}
data := map[string]interface{}{
"User": user,
}
// Send the user a mail with the reset token
mail.SendMailWithTemplate(user.Email, "Reset your password on Vikunja", "reset-password", data)
return
}

27
pkg/models/user_right.go Normal file
View File

@@ -0,0 +1,27 @@
package models
// UserRight defines the rights users can have for lists/namespaces
type UserRight int
// define unknown user right
const (
UserRightUnknown = -1
)
// Enumerate all the user rights
const (
// Can read lists in a User
UserRightRead UserRight = iota
// Can write tasks in a User like lists and todo tasks. Cannot create new lists.
UserRightWrite
// Can manage a list/namespace, can do everything
UserRightAdmin
)
func (r UserRight) isValid() error {
if r != UserRightAdmin && r != UserRightRead && r != UserRightWrite {
return ErrInvalidUserRight{r}
}
return nil
}

View File

@@ -1,6 +1,7 @@
package models
import (
"code.vikunja.io/api/pkg/utils"
"github.com/stretchr/testify/assert"
"testing"
)
@@ -20,24 +21,12 @@ func TestCreateUser(t *testing.T) {
Email: "noone@example.com",
}
// 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)
assert.NoError(t, err)
// Create a second new user
createdUser2, err := CreateUser(User{Username: dummyuser.Username + "2", Email: dummyuser.Email + "m", Password: dummyuser.Password})
_, err = CreateUser(User{Username: dummyuser.Username + "2", Email: dummyuser.Email + "m", Password: dummyuser.Password})
assert.NoError(t, err)
// Check if it fails to create the same user again
@@ -72,14 +61,22 @@ func TestCreateUser(t *testing.T) {
assert.Error(t, err)
assert.True(t, IsErrUserDoesNotExist(err))
// Check the user credentials
// Check the user credentials with an unverified email
user, err := CheckUserCredentials(&UserLogin{"testuu", "1234"})
assert.Error(t, err)
assert.True(t, IsErrEmailNotConfirmed(err))
// Update everything and check again
_, err = x.Cols("is_active").Where("true").Update(User{IsActive: true})
assert.NoError(t, err)
user, err = CheckUserCredentials(&UserLogin{"testuu", "1234"})
assert.NoError(t, err)
assert.Equal(t, "testuu", user.Username)
// Check wrong password (should also fail)
_, err = CheckUserCredentials(&UserLogin{"testuu", "12345"})
assert.Error(t, err)
assert.True(t, IsErrWrongUsernameOrPassword(err))
// Check usercredentials for a nonexistent user (should fail)
_, err = CheckUserCredentials(&UserLogin{"dfstestuu", "1234"})
@@ -128,9 +125,38 @@ func TestCreateUser(t *testing.T) {
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))
}
func TestUserPasswordReset(t *testing.T) {
// Request a new token
tr := &PasswordTokenRequest{
Username: "user1",
}
err := RequestUserPasswordResetToken(tr)
assert.NoError(t, err)
// Get the token / inside the user object
userWithToken, err := GetUserByID(1)
assert.NoError(t, err)
// Try resetting it
reset := &PasswordReset{
Token: userWithToken.PasswordResetToken,
}
// Try resetting it without a password
reset.NewPassword = ""
err = UserPasswordReset(reset)
assert.True(t, IsErrNoUsernamePassword(err))
// Reset it
reset.NewPassword = "1234"
err = UserPasswordReset(reset)
assert.NoError(t, err)
// Try resetting it with a wrong token
reset.Token = utils.MakeRandomString(400)
err = UserPasswordReset(reset)
assert.Error(t, err)
assert.True(t, IsErrInvalidPasswordResetToken(err))
}

View File

@@ -1,7 +1,7 @@
package v1
import (
"code.vikunja.io/api/models"
"code.vikunja.io/api/pkg/models"
"github.com/labstack/echo"
"net/http"
"strconv"

View File

@@ -1,7 +1,8 @@
package v1
import (
"code.vikunja.io/api/models"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/routes/crud"
"crypto/md5"
"encoding/hex"
"github.com/dgrijalva/jwt-go"
@@ -41,7 +42,7 @@ func Login(c echo.Context) error {
// Check user
user, err := models.CheckUserCredentials(&u)
if err != nil {
return c.JSON(http.StatusUnauthorized, models.Message{"Wrong username or password."})
return crud.HandleHTTPError(err)
}
// Create token

View File

@@ -1,6 +1,8 @@
package swagger
import "code.vikunja.io/api/models"
import (
"code.vikunja.io/api/pkg/models"
)
// not actually a response, just a hack to get go-swagger to include definitions
// of the various XYZOption structs
@@ -40,4 +42,13 @@ type swaggerParameterBodies struct {
// in:body
NamespaceUser models.NamespaceUser
// in:body
PasswordReset models.PasswordReset
// in:body
PasswordTokenRequest models.PasswordTokenRequest
// in:body
EmailConfirm models.EmailConfirm
}

View File

@@ -1,6 +1,8 @@
package swagger
import "code.vikunja.io/api/models"
import (
"code.vikunja.io/api/pkg/models"
)
// Message
// swagger:response Message

Some files were not shown because too many files have changed in this diff Show More