mirror of
https://github.com/gotify/server.git
synced 2024-01-28 15:20:56 +03:00
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:
@@ -535,6 +535,6 @@ func fakeImage(t *testing.T, path string) {
|
||||
data, err := ioutil.ReadFile("../test/assets/image.png")
|
||||
assert.Nil(t, err)
|
||||
// Write data to dst
|
||||
err = ioutil.WriteFile(path, data, 0644)
|
||||
err = ioutil.WriteFile(path, data, 0o644)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
34
api/user.go
34
api/user.go
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gotify/server/v2/auth"
|
||||
@@ -59,6 +61,7 @@ type UserAPI struct {
|
||||
DB UserDatabase
|
||||
PasswordStrength int
|
||||
UserChangeNotifier *UserChangeNotifier
|
||||
Registration bool
|
||||
}
|
||||
|
||||
// GetUsers returns all the users
|
||||
@@ -126,11 +129,14 @@ func (a *UserAPI) GetCurrentUser(ctx *gin.Context) {
|
||||
ctx.JSON(200, toExternalUser(user))
|
||||
}
|
||||
|
||||
// CreateUser creates a user
|
||||
// CreateUser create a user.
|
||||
// swagger:operation POST /user user createUser
|
||||
//
|
||||
// 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]
|
||||
// produces: [application/json]
|
||||
@@ -167,6 +173,32 @@ func (a *UserAPI) CreateUser(ctx *gin.Context) {
|
||||
if success := successOrAbort(ctx, 500, err); !success {
|
||||
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 success := successOrAbort(ctx, 500, a.DB.CreateUser(internal)); !success {
|
||||
return
|
||||
|
||||
111
api/user_test.go
111
api/user_test.go
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gotify/server/v2/auth"
|
||||
"github.com/gotify/server/v2/auth/password"
|
||||
"github.com/gotify/server/v2/mode"
|
||||
"github.com/gotify/server/v2/model"
|
||||
@@ -35,7 +36,9 @@ func (s *UserSuite) BeforeTest(suiteName, testName string) {
|
||||
mode.Set(mode.TestDev)
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
||||
|
||||
s.db = testdb.NewDB(s.T())
|
||||
|
||||
s.notifier = new(UserChangeNotifier)
|
||||
s.notifier.OnUserDeleted(func(uint) error {
|
||||
s.notifiedDelete = true
|
||||
@@ -164,15 +167,17 @@ func (s *UserSuite) Test_DeleteUserByID_NotifyFail() {
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_CreateUser() {
|
||||
s.loginAdmin()
|
||||
|
||||
assert.False(s.T(), s.notifiedAdd)
|
||||
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.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)
|
||||
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) {
|
||||
assert.NotNil(s.T(), created)
|
||||
@@ -181,7 +186,88 @@ func (s *UserSuite) Test_CreateUser() {
|
||||
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() {
|
||||
s.loginAdmin()
|
||||
|
||||
s.notifier.OnUserAdded(func(id uint) error {
|
||||
user, err := s.db.GetUserByID(id)
|
||||
if err != nil {
|
||||
@@ -201,6 +287,8 @@ func (s *UserSuite) Test_CreateUser_NotifyFail() {
|
||||
}
|
||||
|
||||
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.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -210,6 +298,8 @@ func (s *UserSuite) Test_CreateUser_NoPassword() {
|
||||
}
|
||||
|
||||
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.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -219,7 +309,8 @@ func (s *UserSuite) Test_CreateUser_NoName() {
|
||||
}
|
||||
|
||||
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.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")))
|
||||
}
|
||||
|
||||
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 {
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx, _ = gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/?%s=%s", key, value), nil)
|
||||
f()(ctx)
|
||||
assert.Equal(s.T(), code, recorder.Code)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *AuthenticationSuite) TestNothingProvided() {
|
||||
@@ -160,13 +161,42 @@ func (s *AuthenticationSuite) TestBasicAuth() {
|
||||
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()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx, _ = gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest("GET", "/", nil)
|
||||
ctx.Request.Header.Set(key, value)
|
||||
f()(ctx)
|
||||
assert.Equal(s.T(), code, recorder.Code)
|
||||
return
|
||||
}
|
||||
|
||||
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.
|
||||
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)
|
||||
if user == nil {
|
||||
userID := ctx.MustGet("userid").(uint)
|
||||
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.
|
||||
|
||||
@@ -29,6 +29,7 @@ func (s *UtilSuite) Test_getID() {
|
||||
assert.Panics(s.T(), func() {
|
||||
s.expectUserIDWith(nil, 0, 0)
|
||||
})
|
||||
s.expectTryUserIDWith(nil, 0, nil)
|
||||
}
|
||||
|
||||
func (s *UtilSuite) Test_getToken() {
|
||||
@@ -44,3 +45,10 @@ func (s *UtilSuite) expectUserIDWith(user *model.User, tokenUserID, expectedID u
|
||||
actualID := GetUserID(ctx)
|
||||
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)
|
||||
uploadedimagesdir: data/images # the directory for storing uploaded images
|
||||
pluginsdir: data/plugins # the directory where plugin resides
|
||||
registration: false # enable registrations
|
||||
|
||||
@@ -51,6 +51,7 @@ type Configuration struct {
|
||||
PassStrength int `default:"10"`
|
||||
UploadedImagesDir string `default:"data/images"`
|
||||
PluginsDir string `default:"data/plugins"`
|
||||
Registration bool `default:"false"`
|
||||
}
|
||||
|
||||
func configFiles() []string {
|
||||
|
||||
@@ -1615,6 +1615,7 @@
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
|
||||
@@ -39,7 +39,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
|
||||
ImageDir: conf.UploadedImagesDir,
|
||||
}
|
||||
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)
|
||||
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")
|
||||
|
||||
// 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.POST("", userHandler.CreateUser)
|
||||
|
||||
authAdmin.DELETE("/:id", userHandler.DeleteUserByID)
|
||||
|
||||
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.
|
||||
func (d *Database) AssertClientNotExist(id uint) {
|
||||
if client, err := d.GetClientByID(id); assert.NoError(d.t, err) {
|
||||
|
||||
@@ -42,6 +42,27 @@ export class CurrentUser {
|
||||
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) => {
|
||||
this.loggedIn = false;
|
||||
this.authenticating = true;
|
||||
|
||||
@@ -67,12 +67,15 @@ class Layout extends React.Component<
|
||||
private version = Layout.defaultVersion;
|
||||
@observable
|
||||
private navOpen = false;
|
||||
@observable
|
||||
private showRegister = true; //TODO https://github.com/gotify/server/pull/394#discussion_r650559205
|
||||
|
||||
private setNavOpen(open: boolean) {
|
||||
this.navOpen = open;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.registration = true; //TODO https://github.com/gotify/server/pull/394#discussion_r650559205
|
||||
if (this.version === Layout.defaultVersion) {
|
||||
axios.get(config.get('url') + 'version').then((resp: AxiosResponse<IVersion>) => {
|
||||
this.version = resp.data.version;
|
||||
@@ -88,7 +91,7 @@ class Layout extends React.Component<
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {version, showSettings, currentTheme} = this;
|
||||
const {version, showSettings, currentTheme, showRegister} = this;
|
||||
const {
|
||||
classes,
|
||||
currentUser: {
|
||||
@@ -101,7 +104,8 @@ class Layout extends React.Component<
|
||||
},
|
||||
} = this.props;
|
||||
const theme = themeMap[currentTheme];
|
||||
const loginRoute = () => (loggedIn ? <Redirect to="/" /> : <Login />);
|
||||
const loginRoute = () =>
|
||||
loggedIn ? <Redirect to="/" /> : <Login showRegister={showRegister} />;
|
||||
return (
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<HashRouter>
|
||||
|
||||
@@ -7,18 +7,25 @@ import DefaultPage from '../common/DefaultPage';
|
||||
import {observable} from 'mobx';
|
||||
import {observer} from 'mobx-react';
|
||||
import {inject, Stores} from '../inject';
|
||||
import RegistrationDialog from './Register';
|
||||
|
||||
type Props = Stores<'currentUser'> & {
|
||||
showRegister: boolean;
|
||||
};
|
||||
|
||||
@observer
|
||||
class Login extends Component<Stores<'currentUser'>> {
|
||||
class Login extends Component<Props> {
|
||||
@observable
|
||||
private username = '';
|
||||
@observable
|
||||
private password = '';
|
||||
@observable
|
||||
private registerDialog = false;
|
||||
|
||||
public render() {
|
||||
const {username, password} = this;
|
||||
const {username, password, registerDialog} = this;
|
||||
return (
|
||||
<DefaultPage title="Login" maxWidth={250}>
|
||||
<DefaultPage title="Login" rightControl={this.registerButton()} maxWidth={250}>
|
||||
<Grid item xs={12} style={{textAlign: 'center'}}>
|
||||
<Container>
|
||||
<form onSubmit={this.preventDefault} id="login-form">
|
||||
@@ -52,6 +59,12 @@ class Login extends Component<Stores<'currentUser'>> {
|
||||
</form>
|
||||
</Container>
|
||||
</Grid>
|
||||
{registerDialog && (
|
||||
<RegistrationDialog
|
||||
fClose={() => (this.registerDialog = false)}
|
||||
fOnSubmit={this.props.currentUser.register}
|
||||
/>
|
||||
)}
|
||||
</DefaultPage>
|
||||
);
|
||||
}
|
||||
@@ -61,6 +74,20 @@ class Login extends Component<Stores<'currentUser'>> {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
99
ui/src/user/Register.tsx
Normal file
99
ui/src/user/Register.tsx
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user