Add registration
Can be enabled via the registration config flag. (disabled per default) Fixes gotify/server#395 Co-authored-by: pigpig <pigpig@pig.pig> Co-authored-by: Karmanyaah Malhotra <32671690+karmanyaahm@users.noreply.github.com> Co-authored-by: Jannis Mattheis <contact@jmattheis.de>
This commit is contained in:
parent
7e261be304
commit
c172590b92
|
|
@ -535,6 +535,6 @@ func fakeImage(t *testing.T, path string) {
|
||||||
data, err := ioutil.ReadFile("../test/assets/image.png")
|
data, err := ioutil.ReadFile("../test/assets/image.png")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
// Write data to dst
|
// Write data to dst
|
||||||
err = ioutil.WriteFile(path, data, 0644)
|
err = ioutil.WriteFile(path, data, 0o644)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
api/user.go
34
api/user.go
|
|
@ -2,6 +2,8 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gotify/server/v2/auth"
|
"github.com/gotify/server/v2/auth"
|
||||||
|
|
@ -59,6 +61,7 @@ type UserAPI struct {
|
||||||
DB UserDatabase
|
DB UserDatabase
|
||||||
PasswordStrength int
|
PasswordStrength int
|
||||||
UserChangeNotifier *UserChangeNotifier
|
UserChangeNotifier *UserChangeNotifier
|
||||||
|
Registration bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsers returns all the users
|
// GetUsers returns all the users
|
||||||
|
|
@ -126,11 +129,14 @@ func (a *UserAPI) GetCurrentUser(ctx *gin.Context) {
|
||||||
ctx.JSON(200, toExternalUser(user))
|
ctx.JSON(200, toExternalUser(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a user
|
// CreateUser create a user.
|
||||||
// swagger:operation POST /user user createUser
|
// swagger:operation POST /user user createUser
|
||||||
//
|
//
|
||||||
// Create a user.
|
// Create a user.
|
||||||
//
|
//
|
||||||
|
// With enabled registration: non admin users can be created without authentication.
|
||||||
|
// With disabled registrations: users can only be created by admin users.
|
||||||
|
//
|
||||||
// ---
|
// ---
|
||||||
// consumes: [application/json]
|
// consumes: [application/json]
|
||||||
// produces: [application/json]
|
// produces: [application/json]
|
||||||
|
|
@ -167,6 +173,32 @@ func (a *UserAPI) CreateUser(ctx *gin.Context) {
|
||||||
if success := successOrAbort(ctx, 500, err); !success {
|
if success := successOrAbort(ctx, 500, err); !success {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requestedBy *model.User
|
||||||
|
uid := auth.TryGetUserID(ctx)
|
||||||
|
if uid != nil {
|
||||||
|
requestedBy, err = a.DB.GetUserByID(*uid)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, fmt.Errorf("could not get user: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestedBy == nil || !requestedBy.Admin {
|
||||||
|
status := http.StatusUnauthorized
|
||||||
|
if requestedBy != nil {
|
||||||
|
status = http.StatusForbidden
|
||||||
|
}
|
||||||
|
if !a.Registration {
|
||||||
|
ctx.AbortWithError(status, errors.New("you are not allowed to access this api"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if internal.Admin {
|
||||||
|
ctx.AbortWithError(status, errors.New("you are not allowed to create an admin user"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if existingUser == nil {
|
if existingUser == nil {
|
||||||
if success := successOrAbort(ctx, 500, a.DB.CreateUser(internal)); !success {
|
if success := successOrAbort(ctx, 500, a.DB.CreateUser(internal)); !success {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
111
api/user_test.go
111
api/user_test.go
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gotify/server/v2/auth"
|
||||||
"github.com/gotify/server/v2/auth/password"
|
"github.com/gotify/server/v2/auth/password"
|
||||||
"github.com/gotify/server/v2/mode"
|
"github.com/gotify/server/v2/mode"
|
||||||
"github.com/gotify/server/v2/model"
|
"github.com/gotify/server/v2/model"
|
||||||
|
|
@ -35,7 +36,9 @@ func (s *UserSuite) BeforeTest(suiteName, testName string) {
|
||||||
mode.Set(mode.TestDev)
|
mode.Set(mode.TestDev)
|
||||||
s.recorder = httptest.NewRecorder()
|
s.recorder = httptest.NewRecorder()
|
||||||
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
||||||
|
|
||||||
s.db = testdb.NewDB(s.T())
|
s.db = testdb.NewDB(s.T())
|
||||||
|
|
||||||
s.notifier = new(UserChangeNotifier)
|
s.notifier = new(UserChangeNotifier)
|
||||||
s.notifier.OnUserDeleted(func(uint) error {
|
s.notifier.OnUserDeleted(func(uint) error {
|
||||||
s.notifiedDelete = true
|
s.notifiedDelete = true
|
||||||
|
|
@ -164,15 +167,17 @@ func (s *UserSuite) Test_DeleteUserByID_NotifyFail() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserSuite) Test_CreateUser() {
|
func (s *UserSuite) Test_CreateUser() {
|
||||||
|
s.loginAdmin()
|
||||||
|
|
||||||
assert.False(s.T(), s.notifiedAdd)
|
assert.False(s.T(), s.notifiedAdd)
|
||||||
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
|
||||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
s.a.CreateUser(s.ctx)
|
s.a.CreateUser(s.ctx)
|
||||||
|
|
||||||
user := &model.UserExternal{ID: 1, Name: "tom", Admin: true}
|
|
||||||
test.BodyEquals(s.T(), user, s.recorder)
|
|
||||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
|
user := &model.UserExternal{ID: 2, Name: "tom", Admin: true}
|
||||||
|
test.BodyEquals(s.T(), user, s.recorder)
|
||||||
|
|
||||||
if created, err := s.db.GetUserByName("tom"); assert.NoError(s.T(), err) {
|
if created, err := s.db.GetUserByName("tom"); assert.NoError(s.T(), err) {
|
||||||
assert.NotNil(s.T(), created)
|
assert.NotNil(s.T(), created)
|
||||||
|
|
@ -181,7 +186,88 @@ func (s *UserSuite) Test_CreateUser() {
|
||||||
assert.True(s.T(), s.notifiedAdd)
|
assert.True(s.T(), s.notifiedAdd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) Test_CreateUser_ByNonAdmin() {
|
||||||
|
s.loginUser()
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": false}`))
|
||||||
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
s.a.CreateUser(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 403, s.recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) Test_CreateUser_Register_ByNonAdmin() {
|
||||||
|
s.loginUser()
|
||||||
|
s.a.Registration = true
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": false}`))
|
||||||
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
s.a.CreateUser(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
|
if created, err := s.db.GetUserByName("tom"); assert.NoError(s.T(), err) {
|
||||||
|
assert.NotNil(s.T(), created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) Test_CreateUser_Register_Admin_ByNonAdmin() {
|
||||||
|
s.a.Registration = true
|
||||||
|
s.loginUser()
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": true}`))
|
||||||
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
s.a.CreateUser(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 403, s.recorder.Code)
|
||||||
|
s.db.AssertUsernameNotExist("tom")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) Test_CreateUser_Anonymous() {
|
||||||
|
s.noLogin()
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": false}`))
|
||||||
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
s.a.CreateUser(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 401, s.recorder.Code)
|
||||||
|
s.db.AssertUsernameNotExist("tom")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) Test_CreateUser_Register_Anonymous() {
|
||||||
|
s.a.Registration = true
|
||||||
|
s.noLogin()
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": false}`))
|
||||||
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
s.a.CreateUser(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||||
|
if created, err := s.db.GetUserByName("tom"); assert.NoError(s.T(), err) {
|
||||||
|
assert.NotNil(s.T(), created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) Test_CreateUser_Register_Admin_Anonymous() {
|
||||||
|
s.a.Registration = true
|
||||||
|
s.noLogin()
|
||||||
|
|
||||||
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "1", "admin": true}`))
|
||||||
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
s.a.CreateUser(s.ctx)
|
||||||
|
|
||||||
|
assert.Equal(s.T(), 401, s.recorder.Code)
|
||||||
|
s.db.AssertUsernameNotExist("tom")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *UserSuite) Test_CreateUser_NotifyFail() {
|
func (s *UserSuite) Test_CreateUser_NotifyFail() {
|
||||||
|
s.loginAdmin()
|
||||||
|
|
||||||
s.notifier.OnUserAdded(func(id uint) error {
|
s.notifier.OnUserAdded(func(id uint) error {
|
||||||
user, err := s.db.GetUserByID(id)
|
user, err := s.db.GetUserByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -201,6 +287,8 @@ func (s *UserSuite) Test_CreateUser_NotifyFail() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserSuite) Test_CreateUser_NoPassword() {
|
func (s *UserSuite) Test_CreateUser_NoPassword() {
|
||||||
|
s.loginAdmin()
|
||||||
|
|
||||||
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "", "admin": true}`))
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "", "admin": true}`))
|
||||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
|
@ -210,6 +298,8 @@ func (s *UserSuite) Test_CreateUser_NoPassword() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserSuite) Test_CreateUser_NoName() {
|
func (s *UserSuite) Test_CreateUser_NoName() {
|
||||||
|
s.loginAdmin()
|
||||||
|
|
||||||
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "", "pass": "asd", "admin": true}`))
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "", "pass": "asd", "admin": true}`))
|
||||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
|
@ -219,7 +309,8 @@ func (s *UserSuite) Test_CreateUser_NoName() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserSuite) Test_CreateUser_NameAlreadyExists() {
|
func (s *UserSuite) Test_CreateUser_NameAlreadyExists() {
|
||||||
s.db.NewUserWithName(1, "tom")
|
s.loginAdmin()
|
||||||
|
s.db.NewUserWithName(2, "tom")
|
||||||
|
|
||||||
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
|
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
|
||||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
@ -333,6 +424,20 @@ func (s *UserSuite) Test_UpdatePassword_EmptyPassword() {
|
||||||
assert.True(s.T(), password.ComparePassword(user.Pass, []byte("old")))
|
assert.True(s.T(), password.ComparePassword(user.Pass, []byte("old")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) loginAdmin() {
|
||||||
|
s.db.CreateUser(&model.User{ID: 1, Name: "admin", Admin: true})
|
||||||
|
auth.RegisterAuthentication(s.ctx, nil, 1, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) loginUser() {
|
||||||
|
s.db.CreateUser(&model.User{ID: 1, Name: "user", Admin: false})
|
||||||
|
auth.RegisterAuthentication(s.ctx, nil, 1, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserSuite) noLogin() {
|
||||||
|
auth.RegisterAuthentication(s.ctx, nil, 0, "")
|
||||||
|
}
|
||||||
|
|
||||||
func externalOf(user *model.User) *model.UserExternal {
|
func externalOf(user *model.User) *model.UserExternal {
|
||||||
return &model.UserExternal{Name: user.Name, Admin: user.Admin, ID: user.ID}
|
return &model.UserExternal{Name: user.Name, Admin: user.Admin, ID: user.ID}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,3 +133,29 @@ func (a *Auth) requireToken(auth authenticate) gin.HandlerFunc {
|
||||||
ctx.AbortWithError(401, errors.New("you need to provide a valid access token or user credentials to access this api"))
|
ctx.AbortWithError(401, errors.New("you need to provide a valid access token or user credentials to access this api"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Auth) Optional() gin.HandlerFunc {
|
||||||
|
return func(ctx *gin.Context) {
|
||||||
|
token := a.tokenFromQueryOrHeader(ctx)
|
||||||
|
user, err := a.userFromBasicAuth(ctx)
|
||||||
|
if err != nil {
|
||||||
|
RegisterAuthentication(ctx, nil, 0, "")
|
||||||
|
ctx.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user != nil {
|
||||||
|
RegisterAuthentication(ctx, user, user.ID, token)
|
||||||
|
ctx.Next()
|
||||||
|
return
|
||||||
|
} else if token != "" {
|
||||||
|
if tokenClient, err := a.DB.GetClientByToken(token); err == nil && tokenClient != nil {
|
||||||
|
RegisterAuthentication(ctx, user, tokenClient.UserID, token)
|
||||||
|
ctx.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RegisterAuthentication(ctx, nil, 0, "")
|
||||||
|
ctx.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,12 +80,13 @@ func (s *AuthenticationSuite) TestQueryToken() {
|
||||||
s.assertQueryRequest("token", "clienttoken_admin", s.auth.RequireAdmin, 200)
|
s.assertQueryRequest("token", "clienttoken_admin", s.auth.RequireAdmin, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthenticationSuite) assertQueryRequest(key, value string, f fMiddleware, code int) {
|
func (s *AuthenticationSuite) assertQueryRequest(key, value string, f fMiddleware, code int) (ctx *gin.Context) {
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ = gin.CreateTestContext(recorder)
|
||||||
ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/?%s=%s", key, value), nil)
|
ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/?%s=%s", key, value), nil)
|
||||||
f()(ctx)
|
f()(ctx)
|
||||||
assert.Equal(s.T(), code, recorder.Code)
|
assert.Equal(s.T(), code, recorder.Code)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthenticationSuite) TestNothingProvided() {
|
func (s *AuthenticationSuite) TestNothingProvided() {
|
||||||
|
|
@ -160,13 +161,42 @@ func (s *AuthenticationSuite) TestBasicAuth() {
|
||||||
s.assertHeaderRequest("Authorization", "Basic bm90ZXhpc3Rpbmc6cHc=", s.auth.RequireAdmin, 401)
|
s.assertHeaderRequest("Authorization", "Basic bm90ZXhpc3Rpbmc6cHc=", s.auth.RequireAdmin, 401)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthenticationSuite) assertHeaderRequest(key, value string, f fMiddleware, code int) {
|
func (s *AuthenticationSuite) TestOptionalAuth() {
|
||||||
|
// various invalid users
|
||||||
|
ctx := s.assertQueryRequest("token", "ergerogerg", s.auth.Optional, 200)
|
||||||
|
assert.Nil(s.T(), TryGetUserID(ctx))
|
||||||
|
ctx = s.assertHeaderRequest("X-Gotify-Key", "ergerogerg", s.auth.Optional, 200)
|
||||||
|
assert.Nil(s.T(), TryGetUserID(ctx))
|
||||||
|
ctx = s.assertHeaderRequest("Authorization", "Basic bm90ZXhpc3Rpbmc6cHc=", s.auth.Optional, 200)
|
||||||
|
assert.Nil(s.T(), TryGetUserID(ctx))
|
||||||
|
ctx = s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHd4", s.auth.Optional, 200)
|
||||||
|
assert.Nil(s.T(), TryGetUserID(ctx))
|
||||||
|
ctx = s.assertQueryRequest("tokenx", "clienttoken", s.auth.Optional, 200)
|
||||||
|
assert.Nil(s.T(), TryGetUserID(ctx))
|
||||||
|
ctx = s.assertQueryRequest("token", "apptoken_admin", s.auth.Optional, 200)
|
||||||
|
assert.Nil(s.T(), TryGetUserID(ctx))
|
||||||
|
|
||||||
|
// user existing:pw
|
||||||
|
ctx = s.assertHeaderRequest("Authorization", "Basic ZXhpc3Rpbmc6cHc=", s.auth.Optional, 200)
|
||||||
|
assert.Equal(s.T(), uint(1), *TryGetUserID(ctx))
|
||||||
|
ctx = s.assertQueryRequest("token", "clienttoken", s.auth.Optional, 200)
|
||||||
|
assert.Equal(s.T(), uint(1), *TryGetUserID(ctx))
|
||||||
|
|
||||||
|
// user admin:pw
|
||||||
|
ctx = s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.Optional, 200)
|
||||||
|
assert.Equal(s.T(), uint(2), *TryGetUserID(ctx))
|
||||||
|
ctx = s.assertQueryRequest("token", "clienttoken_admin", s.auth.Optional, 200)
|
||||||
|
assert.Equal(s.T(), uint(2), *TryGetUserID(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthenticationSuite) assertHeaderRequest(key, value string, f fMiddleware, code int) (ctx *gin.Context) {
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ = gin.CreateTestContext(recorder)
|
||||||
ctx.Request = httptest.NewRequest("GET", "/", nil)
|
ctx.Request = httptest.NewRequest("GET", "/", nil)
|
||||||
ctx.Request.Header.Set(key, value)
|
ctx.Request.Header.Set(key, value)
|
||||||
f()(ctx)
|
f()(ctx)
|
||||||
assert.Equal(s.T(), code, recorder.Code)
|
assert.Equal(s.T(), code, recorder.Code)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type fMiddleware func() gin.HandlerFunc
|
type fMiddleware func() gin.HandlerFunc
|
||||||
|
|
|
||||||
15
auth/util.go
15
auth/util.go
|
|
@ -14,16 +14,25 @@ func RegisterAuthentication(ctx *gin.Context, user *model.User, userID uint, tok
|
||||||
|
|
||||||
// GetUserID returns the user id which was previously registered by RegisterAuthentication.
|
// GetUserID returns the user id which was previously registered by RegisterAuthentication.
|
||||||
func GetUserID(ctx *gin.Context) uint {
|
func GetUserID(ctx *gin.Context) uint {
|
||||||
|
id := TryGetUserID(ctx)
|
||||||
|
if id == nil {
|
||||||
|
panic("token and user may not be null")
|
||||||
|
}
|
||||||
|
return *id
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryGetUserID returns the user id or nil if one is not set.
|
||||||
|
func TryGetUserID(ctx *gin.Context) *uint {
|
||||||
user := ctx.MustGet("user").(*model.User)
|
user := ctx.MustGet("user").(*model.User)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
userID := ctx.MustGet("userid").(uint)
|
userID := ctx.MustGet("userid").(uint)
|
||||||
if userID == 0 {
|
if userID == 0 {
|
||||||
panic("token and user may not be null")
|
return nil
|
||||||
}
|
}
|
||||||
return userID
|
return &userID
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.ID
|
return &user.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTokenID returns the tokenID.
|
// GetTokenID returns the tokenID.
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ func (s *UtilSuite) Test_getID() {
|
||||||
assert.Panics(s.T(), func() {
|
assert.Panics(s.T(), func() {
|
||||||
s.expectUserIDWith(nil, 0, 0)
|
s.expectUserIDWith(nil, 0, 0)
|
||||||
})
|
})
|
||||||
|
s.expectTryUserIDWith(nil, 0, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UtilSuite) Test_getToken() {
|
func (s *UtilSuite) Test_getToken() {
|
||||||
|
|
@ -44,3 +45,10 @@ func (s *UtilSuite) expectUserIDWith(user *model.User, tokenUserID, expectedID u
|
||||||
actualID := GetUserID(ctx)
|
actualID := GetUserID(ctx)
|
||||||
assert.Equal(s.T(), expectedID, actualID)
|
assert.Equal(s.T(), expectedID, actualID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UtilSuite) expectTryUserIDWith(user *model.User, tokenUserID uint, expectedID *uint) {
|
||||||
|
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||||
|
RegisterAuthentication(ctx, user, tokenUserID, "")
|
||||||
|
actualID := TryGetUserID(ctx)
|
||||||
|
assert.Equal(s.T(), expectedID, actualID)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,3 +50,4 @@ defaultuser: # on database creation, gotify creates an admin user
|
||||||
passstrength: 10 # the bcrypt password strength (higher = better but also slower)
|
passstrength: 10 # the bcrypt password strength (higher = better but also slower)
|
||||||
uploadedimagesdir: data/images # the directory for storing uploaded images
|
uploadedimagesdir: data/images # the directory for storing uploaded images
|
||||||
pluginsdir: data/plugins # the directory where plugin resides
|
pluginsdir: data/plugins # the directory where plugin resides
|
||||||
|
registration: false # enable registrations
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ type Configuration struct {
|
||||||
PassStrength int `default:"10"`
|
PassStrength int `default:"10"`
|
||||||
UploadedImagesDir string `default:"data/images"`
|
UploadedImagesDir string `default:"data/images"`
|
||||||
PluginsDir string `default:"data/plugins"`
|
PluginsDir string `default:"data/plugins"`
|
||||||
|
Registration bool `default:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func configFiles() []string {
|
func configFiles() []string {
|
||||||
|
|
|
||||||
|
|
@ -1615,6 +1615,7 @@
|
||||||
"basicAuth": []
|
"basicAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"description": "With enabled registration: non admin users can be created without authentication.\nWith disabled registrations: users can only be created by admin users.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
|
||||||
ImageDir: conf.UploadedImagesDir,
|
ImageDir: conf.UploadedImagesDir,
|
||||||
}
|
}
|
||||||
userChangeNotifier := new(api.UserChangeNotifier)
|
userChangeNotifier := new(api.UserChangeNotifier)
|
||||||
userHandler := api.UserAPI{DB: db, PasswordStrength: conf.PassStrength, UserChangeNotifier: userChangeNotifier}
|
userHandler := api.UserAPI{DB: db, PasswordStrength: conf.PassStrength, UserChangeNotifier: userChangeNotifier, Registration: conf.Registration}
|
||||||
|
|
||||||
pluginManager, err := plugin.NewManager(db, conf.PluginsDir, g.Group("/plugin/:id/custom/"), streamHandler)
|
pluginManager, err := plugin.NewManager(db, conf.PluginsDir, g.Group("/plugin/:id/custom/"), streamHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -82,6 +82,8 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.Group("/user").Use(authentication.Optional()).POST("", userHandler.CreateUser)
|
||||||
|
|
||||||
g.OPTIONS("/*any")
|
g.OPTIONS("/*any")
|
||||||
|
|
||||||
// swagger:operation GET /version version getVersion
|
// swagger:operation GET /version version getVersion
|
||||||
|
|
@ -157,8 +159,6 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
|
||||||
|
|
||||||
authAdmin.GET("", userHandler.GetUsers)
|
authAdmin.GET("", userHandler.GetUsers)
|
||||||
|
|
||||||
authAdmin.POST("", userHandler.CreateUser)
|
|
||||||
|
|
||||||
authAdmin.DELETE("/:id", userHandler.DeleteUserByID)
|
authAdmin.DELETE("/:id", userHandler.DeleteUserByID)
|
||||||
|
|
||||||
authAdmin.GET("/:id", userHandler.GetUserByID)
|
authAdmin.GET("/:id", userHandler.GetUserByID)
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,13 @@ func (d *Database) AssertUserNotExist(id uint) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssertUsernameNotExist asserts that the user does not exist.
|
||||||
|
func (d *Database) AssertUsernameNotExist(name string) {
|
||||||
|
if user, err := d.GetUserByName(name); assert.NoError(d.t, err) {
|
||||||
|
assert.True(d.t, user == nil, "user %d must not exist", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AssertClientNotExist asserts that the client does not exist.
|
// AssertClientNotExist asserts that the client does not exist.
|
||||||
func (d *Database) AssertClientNotExist(id uint) {
|
func (d *Database) AssertClientNotExist(id uint) {
|
||||||
if client, err := d.GetClientByID(id); assert.NoError(d.t, err) {
|
if client, err := d.GetClientByID(id); assert.NoError(d.t, err) {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,27 @@ export class CurrentUser {
|
||||||
window.localStorage.setItem(tokenKey, token);
|
window.localStorage.setItem(tokenKey, token);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public register = async (name: string, pass: string): Promise<boolean> =>
|
||||||
|
axios
|
||||||
|
.create()
|
||||||
|
.post(config.get('url') + 'user', {name, pass})
|
||||||
|
.then(() => {
|
||||||
|
this.snack('User Created. Logging in...');
|
||||||
|
this.login(name, pass);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch((error: AxiosError) => {
|
||||||
|
if (!error || !error.response) {
|
||||||
|
this.snack('No network connection or server unavailable.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const {data} = error.response;
|
||||||
|
this.snack(
|
||||||
|
`Register failed: ${data?.error ?? 'unknown'}: ${data?.errorDescription ?? ''}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
public login = async (username: string, password: string) => {
|
public login = async (username: string, password: string) => {
|
||||||
this.loggedIn = false;
|
this.loggedIn = false;
|
||||||
this.authenticating = true;
|
this.authenticating = true;
|
||||||
|
|
|
||||||
|
|
@ -67,12 +67,15 @@ class Layout extends React.Component<
|
||||||
private version = Layout.defaultVersion;
|
private version = Layout.defaultVersion;
|
||||||
@observable
|
@observable
|
||||||
private navOpen = false;
|
private navOpen = false;
|
||||||
|
@observable
|
||||||
|
private showRegister = true; //TODO https://github.com/gotify/server/pull/394#discussion_r650559205
|
||||||
|
|
||||||
private setNavOpen(open: boolean) {
|
private setNavOpen(open: boolean) {
|
||||||
this.navOpen = open;
|
this.navOpen = open;
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
this.registration = true; //TODO https://github.com/gotify/server/pull/394#discussion_r650559205
|
||||||
if (this.version === Layout.defaultVersion) {
|
if (this.version === Layout.defaultVersion) {
|
||||||
axios.get(config.get('url') + 'version').then((resp: AxiosResponse<IVersion>) => {
|
axios.get(config.get('url') + 'version').then((resp: AxiosResponse<IVersion>) => {
|
||||||
this.version = resp.data.version;
|
this.version = resp.data.version;
|
||||||
|
|
@ -88,7 +91,7 @@ class Layout extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {version, showSettings, currentTheme} = this;
|
const {version, showSettings, currentTheme, showRegister} = this;
|
||||||
const {
|
const {
|
||||||
classes,
|
classes,
|
||||||
currentUser: {
|
currentUser: {
|
||||||
|
|
@ -101,7 +104,8 @@ class Layout extends React.Component<
|
||||||
},
|
},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const theme = themeMap[currentTheme];
|
const theme = themeMap[currentTheme];
|
||||||
const loginRoute = () => (loggedIn ? <Redirect to="/" /> : <Login />);
|
const loginRoute = () =>
|
||||||
|
loggedIn ? <Redirect to="/" /> : <Login showRegister={showRegister} />;
|
||||||
return (
|
return (
|
||||||
<MuiThemeProvider theme={theme}>
|
<MuiThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,25 @@ import DefaultPage from '../common/DefaultPage';
|
||||||
import {observable} from 'mobx';
|
import {observable} from 'mobx';
|
||||||
import {observer} from 'mobx-react';
|
import {observer} from 'mobx-react';
|
||||||
import {inject, Stores} from '../inject';
|
import {inject, Stores} from '../inject';
|
||||||
|
import RegistrationDialog from './Register';
|
||||||
|
|
||||||
|
type Props = Stores<'currentUser'> & {
|
||||||
|
showRegister: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Login extends Component<Stores<'currentUser'>> {
|
class Login extends Component<Props> {
|
||||||
@observable
|
@observable
|
||||||
private username = '';
|
private username = '';
|
||||||
@observable
|
@observable
|
||||||
private password = '';
|
private password = '';
|
||||||
|
@observable
|
||||||
|
private registerDialog = false;
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {username, password} = this;
|
const {username, password, registerDialog} = this;
|
||||||
return (
|
return (
|
||||||
<DefaultPage title="Login" maxWidth={250}>
|
<DefaultPage title="Login" rightControl={this.registerButton()} maxWidth={250}>
|
||||||
<Grid item xs={12} style={{textAlign: 'center'}}>
|
<Grid item xs={12} style={{textAlign: 'center'}}>
|
||||||
<Container>
|
<Container>
|
||||||
<form onSubmit={this.preventDefault} id="login-form">
|
<form onSubmit={this.preventDefault} id="login-form">
|
||||||
|
|
@ -52,6 +59,12 @@ class Login extends Component<Stores<'currentUser'>> {
|
||||||
</form>
|
</form>
|
||||||
</Container>
|
</Container>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{registerDialog && (
|
||||||
|
<RegistrationDialog
|
||||||
|
fClose={() => (this.registerDialog = false)}
|
||||||
|
fOnSubmit={this.props.currentUser.register}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DefaultPage>
|
</DefaultPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +74,20 @@ class Login extends Component<Stores<'currentUser'>> {
|
||||||
this.props.currentUser.login(this.username, this.password);
|
this.props.currentUser.login(this.username, this.password);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private registerButton = () => {
|
||||||
|
if (this.props.showRegister)
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
id="register"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => (this.registerDialog = true)}>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
else return null;
|
||||||
|
};
|
||||||
|
|
||||||
private preventDefault = (e: FormEvent<HTMLFormElement>) => e.preventDefault();
|
private preventDefault = (e: FormEvent<HTMLFormElement>) => e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
|
import DialogActions from '@material-ui/core/DialogActions';
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
|
import React, {ChangeEvent, Component} from 'react';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
name?: string;
|
||||||
|
fClose: VoidFunction;
|
||||||
|
fOnSubmit: (name: string, pass: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
name: string;
|
||||||
|
pass: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RegistrationDialog extends Component<IProps, IState> {
|
||||||
|
public state = {
|
||||||
|
name: '',
|
||||||
|
pass: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {fClose, fOnSubmit} = this.props;
|
||||||
|
const {name, pass} = this.state;
|
||||||
|
const namePresent = this.state.name.length !== 0;
|
||||||
|
const passPresent = this.state.pass.length !== 0;
|
||||||
|
const submitAndClose = (): void => {
|
||||||
|
fOnSubmit(name, pass).then((success) => {
|
||||||
|
if (success) {
|
||||||
|
fClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={true}
|
||||||
|
onClose={fClose}
|
||||||
|
aria-labelledby="form-dialog-title"
|
||||||
|
id="add-edit-user-dialog">
|
||||||
|
<DialogTitle id="form-dialog-title">Registration</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
className="name"
|
||||||
|
label="Name *"
|
||||||
|
type="email"
|
||||||
|
value={name}
|
||||||
|
onChange={this.handleChange.bind(this, 'name')}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
className="password"
|
||||||
|
type="password"
|
||||||
|
value={pass}
|
||||||
|
fullWidth
|
||||||
|
label="Pass *"
|
||||||
|
onChange={this.handleChange.bind(this, 'pass')}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
|
<Tooltip
|
||||||
|
placement={'bottom-start'}
|
||||||
|
title={
|
||||||
|
namePresent
|
||||||
|
? passPresent
|
||||||
|
? ''
|
||||||
|
: 'password is required'
|
||||||
|
: 'name is required'
|
||||||
|
}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="save-create"
|
||||||
|
disabled={!passPresent || !namePresent}
|
||||||
|
onClick={submitAndClose}
|
||||||
|
color="primary"
|
||||||
|
variant="contained">
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleChange(propertyName: string, event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
const state = this.state;
|
||||||
|
state[propertyName] = event.target.value;
|
||||||
|
this.setState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue