Add password reset (#3)
Some checks failed
the build failed

This commit is contained in:
konrad 2018-10-27 09:33:28 +00:00 committed by Gitea
parent 95197ec6ed
commit 0cfea682ea
332 changed files with 53283 additions and 15576 deletions

@ -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
EXTRA_GOFLAGS ?=
LDFLAGS := -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.Tags=$(TAGS)"
@ -49,7 +49,7 @@ all: build
.PHONY: clean
clean:
go clean -i ./...
go clean ./...
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA)
.PHONY: test
@ -133,6 +133,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 +143,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
@ -190,4 +192,4 @@ gocyclo-check:
@hash gocyclo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get 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;

@ -21,4 +21,24 @@ 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 password to reset a password
POST http://localhost:8080/api/v1/user/password/reset
Content-Type: application/json
Accept: application/json
{
"user_id": 1,
"token": "syPYBkzonBbWEXtHQlMDwDMWfsGgkeHWYRBncIDtVBrizTHBGDPnNbpjwtKtKfutUuzCTfQcXLTFgVTzDsmHcPxvrQxlKTmjPyyDLEEwnHkRntsweFyrymjfhiqZwwPCsPLegtnruaaFerjPNgmCXPVjsSGSDWjQcJsVgkljgjeeRwowxYQxMZeLlVHitEHkNfXnXUeEQmPmGLwPuGBGEXhHJpsckYwkOQTulJzDSrsynzNaHRbxQfdxthToFOzidOKzJKdesQKIocTfSDPXzvVKdlSPkZRiyNIbFxoiIWRGQFSHltmqzDwxudwcDbMMwaLQloUWZahhfkFRPKLoFQQezPgYecIihrewglYvQOZfNISKAWyHyWfOBWAkrtGODpuJlTLZwImYzNSX",
"new_password": "1234"
}
###

@ -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'

@ -29,6 +29,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.
@ -60,4 +62,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
```

@ -9,7 +9,8 @@ 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. |
| 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. |

22
go.mod

@ -6,29 +6,45 @@ require (
github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 // indirect
github.com/dgrijalva/jwt-go v0.0.0-20170608005149-a539ee1a749a
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 // indirect
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/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/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/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
)

71
go.sum

@ -2,18 +2,60 @@ 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/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/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 +68,22 @@ 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/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/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 +92,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 +104,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 +123,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 +135,20 @@ 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/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=

@ -2,6 +2,7 @@ package main
import (
"code.vikunja.io/api/models"
"code.vikunja.io/api/models/mail"
"code.vikunja.io/api/routes"
"context"
@ -33,6 +34,9 @@ func main() {
os.Exit(1)
}
// Start the mail daemon
mail.StartMailDaemon()
// Version notification
models.Log.Infof("Vikunja version %s", Version)

@ -17,8 +17,10 @@ func InitConfig() (err error) {
return err
}
// Service
viper.SetDefault("service.JWTSecret", random)
viper.SetDefault("service.interface", ":3456")
viper.SetDefault("service.frontendurl", "")
// Database
viper.SetDefault("database.type", "sqlite")
viper.SetDefault("database.host", "localhost")
@ -34,6 +36,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")

@ -134,25 +134,45 @@ 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{}
// IsErrCannotDeleteLastUser checks if an error is a ErrCannotDeleteLastUser.
func IsErrCannotDeleteLastUser(err error) bool {
_, ok := err.(ErrCannotDeleteLastUser)
return ok
// ErrNoPasswordResetToken represents an error where no password reset token exists for that user
type ErrNoPasswordResetToken struct {
UserID int64
}
func (err ErrCannotDeleteLastUser) Error() string {
return fmt.Sprintf("Cannot delete last user")
func (err ErrNoPasswordResetToken) Error() string {
return fmt.Sprintf("No token to reset a password [UserID: %d]", err.UserID)
}
// ErrCodeCannotDeleteLastUser holds the unique world-error code of this error
const ErrCodeCannotDeleteLastUser = 1007
// ErrCodeNoPasswordResetToken holds the unique world-error code of this error
const ErrCodeNoPasswordResetToken = 1008
// 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 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 {
UserID int64
Token string
}
func (err ErrInvalidPasswordResetToken) Error() string {
return fmt.Sprintf("Invalid token to reset a password [UserID: %d, Token: %s]", err.UserID, 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 provided."}
}
// IsErrInvalidPasswordResetToken checks if an error is a ErrInvalidPasswordResetToken.
func IsErrInvalidPasswordResetToken(err error) bool {
_, ok := err.(ErrInvalidPasswordResetToken)
return ok
}
// ===================

66
models/mail/mail.go Normal file

@ -0,0 +1,66 @@
package mail
import (
"crypto/tls"
"fmt"
"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() {
if viper.GetString("mailer.host") == "" {
//models.Log.Warning("Mailer seems to be not configured! Please see the config docs for more details.")
fmt.Println("Mailer seems to be not configured! Please see the config docs for more details.")
return
}
Queue = make(chan *gomail.Message, viper.GetInt("mailer.queuelength"))
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 {
// models.Log.Error("Error during connect to smtp server: %s", err)
fmt.Printf("Error during connect to smtp server: %s \n", err)
}
open = true
}
if err := gomail.Send(s, m); err != nil {
// models.Log.Error("Error when sending mail: %s", err)
fmt.Printf("Error when sending mail: %s \n", 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 {
fmt.Printf("Error closing the mail server connection: %s\n", err)
}
fmt.Println("Closed connection to mailserver")
open = false
}
}
}
}()
}
// StopMailDaemon closes the mail queue channel, aka stops the daemon
func StopMailDaemon() {
close(Queue)
}

101
models/mail/send_mail.go Normal file

@ -0,0 +1,101 @@
package mail
import (
"bytes"
"code.vikunja.io/api/models/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("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,
Headers: []*header{{Field: "MIME-Version", Content: "1.0"}},
}
SendMail(opts)
}

@ -18,8 +18,11 @@ 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:"-"`
PasswordResetToken string `xorm:"varchar(450)" json:"-"`
Created int64 `xorm:"created" json:"-"`
Updated int64 `xorm:"updated" json:"-"`
}
// TableName returns the table name for users
@ -61,7 +64,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

@ -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

@ -0,0 +1,90 @@
package models
import (
"code.vikunja.io/api/models/mail"
"code.vikunja.io/api/models/utils"
)
// PasswordReset holds the data to reset a password
type PasswordReset struct {
UserID int64 `json:"user_id"`
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 the user exists
user, err := GetUserByID(reset.UserID)
if err != nil {
return
}
// Check if we have a token
exists, err := x.Where("password_reset_token = ? AND id = ?", reset.Token, user.ID).Exist(&User{})
if err != nil {
return
}
if !exists {
return ErrInvalidPasswordResetToken{UserID: reset.UserID, 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
}
// 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
}
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
}

@ -1,6 +1,7 @@
package models
import (
"code.vikunja.io/api/models/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
@ -128,9 +117,39 @@ 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{
UserID: 1,
}
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{
UserID: 1,
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))
}

@ -0,0 +1,36 @@
package utils
import (
"math/rand"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
// MakeRandomString return a random string
func MakeRandomString(n int) string {
b := make([]byte, n)
// A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
for i, cache, remain := n-1, rand.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = rand.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}

@ -1607,6 +1607,82 @@
}
}
},
"/user/password/reset": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Resets a users password",
"operationId": "updatePassword",
"parameters": [
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/PasswordReset"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/Message"
},
"400": {
"$ref": "#/responses/Message"
},
"404": {
"$ref": "#/responses/Message"
},
"500": {
"$ref": "#/responses/Message"
}
}
}
},
"/user/password/token": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Requests a token to reset a users password",
"operationId": "requestUpdatePasswordToken",
"parameters": [
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/PasswordTokenRequest"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/Message"
},
"400": {
"$ref": "#/responses/Message"
},
"404": {
"$ref": "#/responses/Message"
},
"500": {
"$ref": "#/responses/Message"
}
}
}
},
"/users": {
"get": {
"consumes": [
@ -1872,6 +1948,37 @@
},
"x-go-package": "code.vikunja.io/api/models"
},
"PasswordReset": {
"description": "PasswordReset holds the data to reset a password",
"type": "object",
"properties": {
"new_password": {
"type": "string",
"x-go-name": "NewPassword"
},
"token": {
"type": "string",
"x-go-name": "Token"
},
"user_id": {
"type": "integer",
"format": "int64",
"x-go-name": "UserID"
}
},
"x-go-package": "code.vikunja.io/api/models"
},
"PasswordTokenRequest": {
"description": "PasswordTokenRequest defines the request format for password reset resqest",
"type": "object",
"properties": {
"user_name": {
"type": "string",
"x-go-name": "Username"
}
},
"x-go-package": "code.vikunja.io/api/models"
},
"Team": {
"description": "Team holds a team object",
"type": "object",
@ -2176,7 +2283,7 @@
"parameterBodies": {
"description": "parameterBodies",
"schema": {
"$ref": "#/definitions/NamespaceUser"
"$ref": "#/definitions/PasswordTokenRequest"
}
}
}

@ -40,4 +40,10 @@ type swaggerParameterBodies struct {
// in:body
NamespaceUser models.NamespaceUser
// in:body
PasswordReset models.PasswordReset
// in:body
PasswordTokenRequest models.PasswordTokenRequest
}

@ -0,0 +1,84 @@
package v1
import (
"code.vikunja.io/api/models"
"code.vikunja.io/api/routes/crud"
"github.com/labstack/echo"
"net/http"
)
// UserResetPassword is the handler to change a users password
func UserResetPassword(c echo.Context) error {
// swagger:operation POST /user/password/reset user updatePassword
// ---
// summary: Resets a users password
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/PasswordReset"
// responses:
// "200":
// "$ref": "#/responses/Message"
// "400":
// "$ref": "#/responses/Message"
// "404":
// "$ref": "#/responses/Message"
// "500":
// "$ref": "#/responses/Message"
// Check for Request Content
var pwReset models.PasswordReset
if err := c.Bind(&pwReset); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
}
err := models.UserPasswordReset(&pwReset)
if err != nil {
return crud.HandleHTTPError(err)
}
return c.JSON(http.StatusOK, models.Message{"The password was updated successfully."})
}
// UserRequestResetPasswordToken is the handler to change a users password
func UserRequestResetPasswordToken(c echo.Context) error {
// swagger:operation POST /user/password/token user requestUpdatePasswordToken
// ---
// summary: Requests a token to reset a users password
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/PasswordTokenRequest"
// responses:
// "200":
// "$ref": "#/responses/Message"
// "400":
// "$ref": "#/responses/Message"
// "404":
// "$ref": "#/responses/Message"
// "500":
// "$ref": "#/responses/Message"
// Check for Request Content
var pwTokenReset models.PasswordTokenRequest
if err := c.Bind(&pwTokenReset); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "No user ID provided.")
}
err := models.RequestUserPasswordResetToken(&pwTokenReset)
if err != nil {
return crud.HandleHTTPError(err)
}
return c.JSON(http.StatusOK, models.Message{"Token was sent."})
}

@ -65,6 +65,8 @@ func RegisterRoutes(e *echo.Echo) {
a.POST("/login", apiv1.Login)
a.POST("/register", apiv1.RegisterUser)
a.POST("/user/password/token", apiv1.UserRequestResetPasswordToken)
a.POST("/user/password/reset", apiv1.UserResetPassword)
// ===== Routes with Authetification =====
// Authetification

@ -0,0 +1,5 @@
</div>
</div>
</div>
</body>
</html>

@ -0,0 +1,12 @@
<!doctype html>
<html style="width: 100%; height: 100%; padding: 0; margin: 0;">
<head>
<meta name="viewport" content="width: display-width;">
</head>
<body style="width: 100%; padding: 0; margin: 0; background: #fbfbfb">
<div style="width: 100%; font-family: 'Open Sans', sans-serif; text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; text-align: justify;">
<h1 style="font-size: 30px; text-align: center;">
<img src="{{.FrontendURL}}images/logo-full.svg" style="height: 75px;" alt="Vikunja"/>
</h1>
<div style="border: 1px solid #ccc; box-shadow: 1px 1px 5px #eeeeee; padding: 5px 25px; border-radius: 3px; background: #fff;">

@ -0,0 +1,9 @@
{{template "mail-header.tmpl" .}}
<p>
Hi {{.User.Username}},<br/>
<br/>
Your account password was successfully changed.
<br/>
If this wasn't you, it could mean someone compromised your account. In this case contact your server's administrator.
</p>
{{template "mail-footer.tmpl"}}

@ -0,0 +1,5 @@
Hi {{.User.Username}},
Your account password was successfully changed.
If this wasn't you, it could mean someone compromised your account. In this case contact your server's administrator.

@ -0,0 +1,14 @@
{{template "mail-header.tmpl" .}}
<p>
Hi {{.User.Username}},<br>
<br>
To reset your password, click the link below:
</p>
<a href="{{.FrontendURL}}?userPasswordReset={{.User.PasswordResetToken}}" title="Reset your password" style="background: rgb(20, 131, 175); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; border: 1px solid rgb(16, 106, 140); border-bottom-width: 3px; color: rgb(255, 255, 255); font-weight: 700; font-size: 13px; margin: 10px auto; padding: 5px 10px; text-decoration: none; text-align: center; text-rendering: optimizelegibility; text-transform: uppercase; display: block; width: 200px;">
Reset your password
</a>
<p>
If the button above doesn't work, copy the url below and paste it in your browsers address bar:<br/>
{{.FrontendURL}}?userPasswordReset={{.User.PasswordResetToken}}
</p>
{{template "mail-footer.tmpl"}}

@ -0,0 +1,3 @@
Hi {{.User.Username}},
Use the following link to reset your password: {{.FrontendURL}}?userPasswordReset={{.User.PasswordResetToken}}

@ -2,7 +2,7 @@ ISC License
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
Permission to use, copy, modify, and distribute this software for any
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

@ -16,7 +16,9 @@
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// +build !js,!appengine,!safe,!disableunsafe
// Go versions prior to 1.4 are disabled because they use a different layout
// for interfaces which make the implementation of unsafeReflectValue more complex.
// +build !js,!appengine,!safe,!disableunsafe,go1.4
package spew
@ -34,80 +36,49 @@ const (
ptrSize = unsafe.Sizeof((*byte)(nil))
)
var (
// offsetPtr, offsetScalar, and offsetFlag are the offsets for the
// internal reflect.Value fields. These values are valid before golang
// commit ecccf07e7f9d which changed the format. The are also valid
// after commit 82f48826c6c7 which changed the format again to mirror
// the original format. Code in the init function updates these offsets
// as necessary.
offsetPtr = uintptr(ptrSize)
offsetScalar = uintptr(0)
offsetFlag = uintptr(ptrSize * 2)
type flag uintptr
// flagKindWidth and flagKindShift indicate various bits that the
// reflect package uses internally to track kind information.
//
// flagRO indicates whether or not the value field of a reflect.Value is
// read-only.
//
// flagIndir indicates whether the value field of a reflect.Value is
// the actual data or a pointer to the data.
//
// These values are valid before golang commit 90a7c3c86944 which
// changed their positions. Code in the init function updates these
// flags as necessary.
flagKindWidth = uintptr(5)
flagKindShift = uintptr(flagKindWidth - 1)
flagRO = uintptr(1 << 0)
flagIndir = uintptr(1 << 1)
var (
// flagRO indicates whether the value field of a reflect.Value
// is read-only.
flagRO flag
// flagAddr indicates whether the address of the reflect.Value's
// value may be taken.
flagAddr flag
)
func init() {
// Older versions of reflect.Value stored small integers directly in the
// ptr field (which is named val in the older versions). Versions
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
// scalar for this purpose which unfortunately came before the flag
// field, so the offset of the flag field is different for those
// versions.
//
// This code constructs a new reflect.Value from a known small integer
// and checks if the size of the reflect.Value struct indicates it has
// the scalar field. When it does, the offsets are updated accordingly.
vv := reflect.ValueOf(0xf00)
if unsafe.Sizeof(vv) == (ptrSize * 4) {
offsetScalar = ptrSize * 2
offsetFlag = ptrSize * 3
}
// flagKindMask holds the bits that make up the kind
// part of the flags field. In all the supported versions,
// it is in the lower 5 bits.
const flagKindMask = flag(0x1f)
// Commit 90a7c3c86944 changed the flag positions such that the low
// order bits are the kind. This code extracts the kind from the flags
// field and ensures it's the correct type. When it's not, the flag
// order has been changed to the newer format, so the flags are updated
// accordingly.
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
upfv := *(*uintptr)(upf)
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift)
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) {
flagKindShift = 0
flagRO = 1 << 5
flagIndir = 1 << 6
// Different versions of Go have used different
// bit layouts for the flags type. This table
// records the known combinations.
var okFlags = []struct {
ro, addr flag
}{{
// From Go 1.4 to 1.5
ro: 1 << 5,
addr: 1 << 7,
}, {
// Up to Go tip.
ro: 1<<5 | 1<<6,
addr: 1 << 8,
}}
// Commit adf9b30e5594 modified the flags to separate the
// flagRO flag into two bits which specifies whether or not the
// field is embedded. This causes flagIndir to move over a bit
// and means that flagRO is the combination of either of the
// original flagRO bit and the new bit.
//
// This code detects the change by extracting what used to be
// the indirect bit to ensure it's set. When it's not, the flag
// order has been changed to the newer format, so the flags are
// updated accordingly.
if upfv&flagIndir == 0 {
flagRO = 3 << 5
flagIndir = 1 << 7
}
var flagValOffset = func() uintptr {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
return field.Offset
}()
// flagField returns a pointer to the flag field of a reflect.Value.
func flagField(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
@ -119,34 +90,56 @@ func init() {
// This allows us to check for implementations of the Stringer and error
// interfaces to be used for pretty printing ordinarily unaddressable and
// inaccessible values such as unexported struct fields.
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
indirects := 1
vt := v.Type()
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
if rvf&flagIndir != 0 {
vt = reflect.PtrTo(v.Type())
indirects++
} else if offsetScalar != 0 {
// The value is in the scalar field when it's not one of the
// reference types.
switch vt.Kind() {
case reflect.Uintptr:
case reflect.Chan:
case reflect.Func:
case reflect.Map:
case reflect.Ptr:
case reflect.UnsafePointer:
default:
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
offsetScalar)
func unsafeReflectValue(v reflect.Value) reflect.Value {
if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
return v
}
flagFieldPtr := flagField(&v)
*flagFieldPtr &^= flagRO
*flagFieldPtr |= flagAddr
return v
}
// Sanity checks against future reflect package changes
// to the type or semantics of the Value.flag field.
func init() {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
panic("reflect.Value flag field has changed kind")
}
type t0 int
var t struct {
A t0