You've already forked focalboard
mirror of
https://github.com/mattermost/focalboard.git
synced 2025-07-12 23:50:27 +02:00
Require signup token to register
This commit is contained in:
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/mattermost/mattermost-octo-tasks/server/app"
|
"github.com/mattermost/mattermost-octo-tasks/server/app"
|
||||||
"github.com/mattermost/mattermost-octo-tasks/server/model"
|
"github.com/mattermost/mattermost-octo-tasks/server/model"
|
||||||
|
"github.com/mattermost/mattermost-octo-tasks/server/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------------------------------
|
||||||
@ -53,6 +54,9 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
|||||||
|
|
||||||
r.HandleFunc("/api/v1/sharing/{rootID}", a.sessionRequired(a.handlePostSharing)).Methods("POST")
|
r.HandleFunc("/api/v1/sharing/{rootID}", a.sessionRequired(a.handlePostSharing)).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/sharing/{rootID}", a.handleGetSharing).Methods("GET")
|
r.HandleFunc("/api/v1/sharing/{rootID}", a.handleGetSharing).Methods("GET")
|
||||||
|
|
||||||
|
r.HandleFunc("/api/v1/workspace", a.sessionRequired(a.handleGetWorkspace)).Methods("GET")
|
||||||
|
r.HandleFunc("/api/v1/workspace/regenerate_signup_token", a.sessionRequired(a.handlePostWorkspaceRegenerateSignupToken)).Methods("POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -427,7 +431,6 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(`ERROR: %v`, r)
|
log.Printf(`ERROR: %v`, r)
|
||||||
errorResponse(w, http.StatusInternalServerError, nil)
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,7 +438,6 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(`ERROR: %v`, r)
|
log.Printf(`ERROR: %v`, r)
|
||||||
errorResponse(w, http.StatusInternalServerError, nil)
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,7 +449,6 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
|
|||||||
requestBody, err := ioutil.ReadAll(r.Body)
|
requestBody, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorResponse(w, http.StatusInternalServerError, nil)
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,7 +457,6 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
|
|||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
log.Printf(`ERROR: %v`, r)
|
log.Printf(`ERROR: %v`, r)
|
||||||
errorResponse(w, http.StatusInternalServerError, nil)
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -466,7 +466,6 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
|
|||||||
err = json.Unmarshal(requestBody, &sharing)
|
err = json.Unmarshal(requestBody, &sharing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorResponse(w, http.StatusInternalServerError, nil)
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,7 +482,6 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf(`ERROR: %v, REQUEST: %v`, err, r)
|
log.Printf(`ERROR: %v, REQUEST: %v`, err, r)
|
||||||
errorResponse(w, http.StatusInternalServerError, nil)
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,6 +489,46 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
|
|||||||
jsonStringResponse(w, http.StatusOK, "{}")
|
jsonStringResponse(w, http.StatusOK, "{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workspace
|
||||||
|
|
||||||
|
func (a *API) handleGetWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||||
|
workspace, err := a.app().GetRootWorkspace()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(`ERROR: %v`, r)
|
||||||
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceData, err := json.Marshal(workspace)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(`ERROR: %v`, r)
|
||||||
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonStringResponse(w, http.StatusOK, string(workspaceData))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
workspace, err := a.app().GetRootWorkspace()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(`ERROR: %v`, r)
|
||||||
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.SignupToken = utils.CreateGUID()
|
||||||
|
|
||||||
|
err = a.app().UpsertWorkspaceSignupToken(*workspace)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(`ERROR: %v`, r)
|
||||||
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonStringResponse(w, http.StatusOK, "{}")
|
||||||
|
}
|
||||||
|
|
||||||
// File upload
|
// File upload
|
||||||
|
|
||||||
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -26,6 +26,7 @@ type RegisterData struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rd *RegisterData) IsValid() error {
|
func (rd *RegisterData) IsValid() error {
|
||||||
@ -77,14 +78,12 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorResponse(w, http.StatusInternalServerError, map[string]string{"error": "Unknown login type"})
|
errorResponse(w, http.StatusInternalServerError, map[string]string{"error": "Unknown login type"})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||||
requestBody, err := ioutil.ReadAll(r.Body)
|
requestBody, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorResponse(w, http.StatusInternalServerError, nil)
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +94,33 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate token
|
||||||
|
if len(registerData.Token) > 0 {
|
||||||
|
workspace, err := a.app().GetRootWorkspace()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERROR: Unable to get active user count", err)
|
||||||
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if registerData.Token != workspace.SignupToken {
|
||||||
|
errorResponse(w, http.StatusUnauthorized, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No signup token, check if no active users
|
||||||
|
userCount, err := a.app().GetActiveUserCount()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERROR: Unable to get active user count", err)
|
||||||
|
errorResponse(w, http.StatusInternalServerError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userCount > 0 {
|
||||||
|
errorResponse(w, http.StatusUnauthorized, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err = registerData.IsValid(); err != nil {
|
if err = registerData.IsValid(); err != nil {
|
||||||
errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@ -105,8 +131,8 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|||||||
errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonBytesResponse(w, http.StatusOK, nil)
|
jsonBytesResponse(w, http.StatusOK, nil)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) sessionRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
|
func (a *API) sessionRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -27,6 +27,11 @@ func (a *App) GetSession(token string) (*model.Session, error) {
|
|||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetActiveUserCount returns the number of active users
|
||||||
|
func (a *App) GetActiveUserCount() (int, error) {
|
||||||
|
return a.store.GetActiveUserCount()
|
||||||
|
}
|
||||||
|
|
||||||
// GetUser Get an existing active user by id
|
// GetUser Get an existing active user by id
|
||||||
func (a *App) GetUser(ID string) (*model.User, error) {
|
func (a *App) GetUser(ID string) (*model.User, error) {
|
||||||
if len(ID) < 1 {
|
if len(ID) < 1 {
|
||||||
|
53
server/app/workspaces.go
Normal file
53
server/app/workspaces.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-octo-tasks/server/model"
|
||||||
|
"github.com/mattermost/mattermost-octo-tasks/server/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *App) GetRootWorkspace() (*model.Workspace, error) {
|
||||||
|
workspaceID := "0"
|
||||||
|
workspace, _ := a.store.GetWorkspace(workspaceID)
|
||||||
|
if workspace == nil {
|
||||||
|
workspace = &model.Workspace{
|
||||||
|
ID: workspaceID,
|
||||||
|
SignupToken: utils.CreateGUID(),
|
||||||
|
}
|
||||||
|
err := a.store.UpsertWorkspaceSignupToken(*workspace)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Unable to initialize workspace", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
workspace, err = a.store.GetWorkspace(workspaceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Unable to get initialized workspace", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("initialized workspace")
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) getWorkspace(ID string) (*model.Workspace, error) {
|
||||||
|
workspace, err := a.store.GetWorkspace(ID)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return workspace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) UpsertWorkspaceSettings(workspace model.Workspace) error {
|
||||||
|
return a.store.UpsertWorkspaceSettings(workspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) UpsertWorkspaceSignupToken(workspace model.Workspace) error {
|
||||||
|
return a.store.UpsertWorkspaceSignupToken(workspace)
|
||||||
|
}
|
9
server/model/workspace.go
Normal file
9
server/model/workspace.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Workspace struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SignupToken string `json:"signupToken"`
|
||||||
|
Settings map[string]interface{} `json:"settings"`
|
||||||
|
ModifiedBy string `json:"modifiedBy"`
|
||||||
|
UpdateAt int64 `json:"updateAt"`
|
||||||
|
}
|
@ -47,7 +47,6 @@ func New(cfg *config.Configuration, singleUser bool) (*Server, error) {
|
|||||||
store, err := sqlstore.New(cfg.DBType, cfg.DBConfigString)
|
store, err := sqlstore.New(cfg.DBType, cfg.DBConfigString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Unable to start the database", err)
|
log.Fatal("Unable to start the database", err)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +67,9 @@ func New(cfg *config.Configuration, singleUser bool) (*Server, error) {
|
|||||||
appBuilder := func() *app.App { return app.New(cfg, store, wsServer, filesBackend, webhookClient) }
|
appBuilder := func() *app.App { return app.New(cfg, store, wsServer, filesBackend, webhookClient) }
|
||||||
api := api.NewAPI(appBuilder, singleUser)
|
api := api.NewAPI(appBuilder, singleUser)
|
||||||
|
|
||||||
|
// Init workspace
|
||||||
|
appBuilder().GetRootWorkspace()
|
||||||
|
|
||||||
webServer := web.NewServer(cfg.WebPath, cfg.Port, cfg.UseSSL)
|
webServer := web.NewServer(cfg.WebPath, cfg.Port, cfg.UseSSL)
|
||||||
webServer.AddRoutes(wsServer)
|
webServer.AddRoutes(wsServer)
|
||||||
webServer.AddRoutes(api)
|
webServer.AddRoutes(api)
|
||||||
|
@ -313,6 +313,21 @@ func (mr *MockStoreMockRecorder) GetUserByUsername(arg0 interface{}) *gomock.Cal
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockStore)(nil).GetUserByUsername), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockStore)(nil).GetUserByUsername), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWorkspace mocks base method
|
||||||
|
func (m *MockStore) GetWorkspace(arg0 string) (*model.Workspace, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetWorkspace", arg0)
|
||||||
|
ret0, _ := ret[0].(*model.Workspace)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorkspace indicates an expected call of GetWorkspace
|
||||||
|
func (mr *MockStoreMockRecorder) GetWorkspace(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspace", reflect.TypeOf((*MockStore)(nil).GetWorkspace), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// InsertBlock mocks base method
|
// InsertBlock mocks base method
|
||||||
func (m *MockStore) InsertBlock(arg0 model.Block) error {
|
func (m *MockStore) InsertBlock(arg0 model.Block) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -410,3 +425,31 @@ func (mr *MockStoreMockRecorder) UpsertSharing(arg0 interface{}) *gomock.Call {
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertSharing", reflect.TypeOf((*MockStore)(nil).UpsertSharing), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertSharing", reflect.TypeOf((*MockStore)(nil).UpsertSharing), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpsertWorkspaceSettings mocks base method
|
||||||
|
func (m *MockStore) UpsertWorkspaceSettings(arg0 model.Workspace) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpsertWorkspaceSettings", arg0)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertWorkspaceSettings indicates an expected call of UpsertWorkspaceSettings
|
||||||
|
func (mr *MockStoreMockRecorder) UpsertWorkspaceSettings(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceSettings", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceSettings), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertWorkspaceSignupToken mocks base method
|
||||||
|
func (m *MockStore) UpsertWorkspaceSignupToken(arg0 model.Workspace) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpsertWorkspaceSignupToken", arg0)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertWorkspaceSignupToken indicates an expected call of UpsertWorkspaceSignupToken
|
||||||
|
func (mr *MockStoreMockRecorder) UpsertWorkspaceSignupToken(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceSignupToken", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceSignupToken), arg0)
|
||||||
|
}
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
// postgres_files/000005_blocks_modifiedby.up.sql
|
// postgres_files/000005_blocks_modifiedby.up.sql
|
||||||
// postgres_files/000006_sharing_table.down.sql
|
// postgres_files/000006_sharing_table.down.sql
|
||||||
// postgres_files/000006_sharing_table.up.sql
|
// postgres_files/000006_sharing_table.up.sql
|
||||||
|
// postgres_files/000007_workspaces_table.down.sql
|
||||||
|
// postgres_files/000007_workspaces_table.up.sql
|
||||||
package postgres
|
package postgres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -292,7 +294,7 @@ func _000006_sharing_tableDownSql() (*asset, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
info := bindataFileInfo{name: "000006_sharing_table.down.sql", size: 20, mode: os.FileMode(420), modTime: time.Unix(1610482431, 0)}
|
info := bindataFileInfo{name: "000006_sharing_table.down.sql", size: 20, mode: os.FileMode(420), modTime: time.Unix(1610576067, 0)}
|
||||||
a := &asset{bytes: bytes, info: info}
|
a := &asset{bytes: bytes, info: info}
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
@ -312,7 +314,47 @@ func _000006_sharing_tableUpSql() (*asset, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
info := bindataFileInfo{name: "000006_sharing_table.up.sql", size: 159, mode: os.FileMode(420), modTime: time.Unix(1610483324, 0)}
|
info := bindataFileInfo{name: "000006_sharing_table.up.sql", size: 159, mode: os.FileMode(420), modTime: time.Unix(1610576067, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var __000007_workspaces_tableDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x28\xcf\x2f\xca\x2e\x2e\x48\x4c\x4e\x2d\xb6\xe6\x02\x04\x00\x00\xff\xff\xc4\x05\x92\x8e\x17\x00\x00\x00")
|
||||||
|
|
||||||
|
func _000007_workspaces_tableDownSqlBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
__000007_workspaces_tableDownSql,
|
||||||
|
"000007_workspaces_table.down.sql",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _000007_workspaces_tableDownSql() (*asset, error) {
|
||||||
|
bytes, err := _000007_workspaces_tableDownSqlBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "000007_workspaces_table.down.sql", size: 23, mode: os.FileMode(420), modTime: time.Unix(1610576169, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var __000007_workspaces_tableUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\xcc\xc1\x6a\x83\x30\x00\x06\xe0\x73\xf2\x14\xff\xd1\x40\x0e\x8e\xc1\x2e\x3b\x45\xc9\xb6\x6c\x2e\x96\x98\x96\x7a\x12\xdb\xa4\x12\xa4\x2a\x4d\xa4\xf4\xed\x0b\x3d\xf4\xd0\xf3\x07\x5f\x69\xa4\xb0\x12\x56\x14\x95\x84\xfa\x82\xae\x2d\xe4\x5e\x35\xb6\xc1\x75\xbe\x8c\x71\xe9\x8f\x3e\x22\xa3\x24\x38\xec\x84\x29\x7f\x84\xc9\xde\x3f\x18\xa7\x24\x86\x61\x5a\x97\x2e\xcd\xa3\x9f\x9e\xf4\x96\xe7\xec\x71\xe8\x6d\x55\x71\x0a\x00\xd1\xa7\x14\xa6\x21\xe2\xb7\xa9\x35\xa7\xe4\x3c\xbb\x70\x0a\xde\x75\x87\xdb\xcb\xb8\x2e\xae\x4f\xbe\xeb\x13\x0a\xf5\xad\xb4\xe5\x94\x6c\x8c\xfa\x17\xa6\xc5\x9f\x6c\x91\x05\xc7\x28\xfb\xa4\xf7\x00\x00\x00\xff\xff\x3b\x70\x91\x2c\xb3\x00\x00\x00")
|
||||||
|
|
||||||
|
func _000007_workspaces_tableUpSqlBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
__000007_workspaces_tableUpSql,
|
||||||
|
"000007_workspaces_table.up.sql",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _000007_workspaces_tableUpSql() (*asset, error) {
|
||||||
|
bytes, err := _000007_workspaces_tableUpSqlBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "000007_workspaces_table.up.sql", size: 179, mode: os.FileMode(420), modTime: time.Unix(1610577228, 0)}
|
||||||
a := &asset{bytes: bytes, info: info}
|
a := &asset{bytes: bytes, info: info}
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
@ -381,6 +423,8 @@ var _bindata = map[string]func() (*asset, error){
|
|||||||
"000005_blocks_modifiedby.up.sql": _000005_blocks_modifiedbyUpSql,
|
"000005_blocks_modifiedby.up.sql": _000005_blocks_modifiedbyUpSql,
|
||||||
"000006_sharing_table.down.sql": _000006_sharing_tableDownSql,
|
"000006_sharing_table.down.sql": _000006_sharing_tableDownSql,
|
||||||
"000006_sharing_table.up.sql": _000006_sharing_tableUpSql,
|
"000006_sharing_table.up.sql": _000006_sharing_tableUpSql,
|
||||||
|
"000007_workspaces_table.down.sql": _000007_workspaces_tableDownSql,
|
||||||
|
"000007_workspaces_table.up.sql": _000007_workspaces_tableUpSql,
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssetDir returns the file names below a certain
|
// AssetDir returns the file names below a certain
|
||||||
@ -435,6 +479,8 @@ var _bintree = &bintree{nil, map[string]*bintree{
|
|||||||
"000005_blocks_modifiedby.up.sql": &bintree{_000005_blocks_modifiedbyUpSql, map[string]*bintree{}},
|
"000005_blocks_modifiedby.up.sql": &bintree{_000005_blocks_modifiedbyUpSql, map[string]*bintree{}},
|
||||||
"000006_sharing_table.down.sql": &bintree{_000006_sharing_tableDownSql, map[string]*bintree{}},
|
"000006_sharing_table.down.sql": &bintree{_000006_sharing_tableDownSql, map[string]*bintree{}},
|
||||||
"000006_sharing_table.up.sql": &bintree{_000006_sharing_tableUpSql, map[string]*bintree{}},
|
"000006_sharing_table.up.sql": &bintree{_000006_sharing_tableUpSql, map[string]*bintree{}},
|
||||||
|
"000007_workspaces_table.down.sql": &bintree{_000007_workspaces_tableDownSql, map[string]*bintree{}},
|
||||||
|
"000007_workspaces_table.up.sql": &bintree{_000007_workspaces_tableUpSql, map[string]*bintree{}},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// RestoreAsset restores an asset under the given directory
|
// RestoreAsset restores an asset under the given directory
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE workspaces;
|
@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS workspaces (
|
||||||
|
id VARCHAR(36),
|
||||||
|
signup_token VARCHAR(100) NOT NULL,
|
||||||
|
settings JSON,
|
||||||
|
modified_by VARCHAR(36),
|
||||||
|
update_at BIGINT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
@ -12,6 +12,8 @@
|
|||||||
// sqlite_files/000005_blocks_modifiedby.up.sql
|
// sqlite_files/000005_blocks_modifiedby.up.sql
|
||||||
// sqlite_files/000006_sharing_table.down.sql
|
// sqlite_files/000006_sharing_table.down.sql
|
||||||
// sqlite_files/000006_sharing_table.up.sql
|
// sqlite_files/000006_sharing_table.up.sql
|
||||||
|
// sqlite_files/000007_workspaces_table.down.sql
|
||||||
|
// sqlite_files/000007_workspaces_table.up.sql
|
||||||
package sqlite
|
package sqlite
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -292,7 +294,7 @@ func _000006_sharing_tableDownSql() (*asset, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
info := bindataFileInfo{name: "000006_sharing_table.down.sql", size: 20, mode: os.FileMode(420), modTime: time.Unix(1610482438, 0)}
|
info := bindataFileInfo{name: "000006_sharing_table.down.sql", size: 20, mode: os.FileMode(420), modTime: time.Unix(1610576067, 0)}
|
||||||
a := &asset{bytes: bytes, info: info}
|
a := &asset{bytes: bytes, info: info}
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
@ -312,7 +314,47 @@ func _000006_sharing_tableUpSql() (*asset, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
info := bindataFileInfo{name: "000006_sharing_table.up.sql", size: 159, mode: os.FileMode(420), modTime: time.Unix(1610483328, 0)}
|
info := bindataFileInfo{name: "000006_sharing_table.up.sql", size: 159, mode: os.FileMode(420), modTime: time.Unix(1610576067, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var __000007_workspaces_tableDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x28\xcf\x2f\xca\x2e\x2e\x48\x4c\x4e\x2d\xb6\xe6\x02\x04\x00\x00\xff\xff\xc4\x05\x92\x8e\x17\x00\x00\x00")
|
||||||
|
|
||||||
|
func _000007_workspaces_tableDownSqlBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
__000007_workspaces_tableDownSql,
|
||||||
|
"000007_workspaces_table.down.sql",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _000007_workspaces_tableDownSql() (*asset, error) {
|
||||||
|
bytes, err := _000007_workspaces_tableDownSqlBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "000007_workspaces_table.down.sql", size: 23, mode: os.FileMode(420), modTime: time.Unix(1610576588, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var __000007_workspaces_tableUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\xcc\x41\xcb\x82\x30\x00\x87\xf1\xb3\xfb\x14\xff\xa3\x03\x0f\xbe\xbc\xd0\xa5\xd3\x94\x55\x23\xb3\x98\x2b\xf4\x24\xd6\x96\x0c\x49\x47\x9b\x44\xdf\x3e\xea\xd0\xa1\xf3\xf3\xf0\xcb\x25\x67\x8a\x43\xb1\xac\xe0\x10\x2b\x94\x7b\x05\x5e\x8b\x4a\x55\x78\x4c\xf7\xc1\xbb\xee\x62\x3c\x62\x12\x59\x8d\x13\x93\xf9\x86\xc9\xf8\x7f\x41\x13\x12\x79\xdb\x8f\xb3\x6b\xc3\x34\x98\xf1\x9b\xfe\xd2\x94\x7e\x8c\xf2\x58\x14\x09\x01\x00\x6f\x42\xb0\x63\xef\xa1\x78\xad\x12\x12\xdd\x26\x6d\xaf\xd6\xe8\xf6\xfc\xfc\x11\x67\xa7\xbb\x60\xda\x2e\x20\x13\x6b\x51\xbe\xe7\x83\x14\x3b\x26\x1b\x6c\x79\x83\xd8\x6a\x4a\xe8\x92\xbc\x02\x00\x00\xff\xff\xa0\xd9\x01\x00\xb3\x00\x00\x00")
|
||||||
|
|
||||||
|
func _000007_workspaces_tableUpSqlBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
__000007_workspaces_tableUpSql,
|
||||||
|
"000007_workspaces_table.up.sql",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _000007_workspaces_tableUpSql() (*asset, error) {
|
||||||
|
bytes, err := _000007_workspaces_tableUpSqlBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "000007_workspaces_table.up.sql", size: 179, mode: os.FileMode(420), modTime: time.Unix(1610577231, 0)}
|
||||||
a := &asset{bytes: bytes, info: info}
|
a := &asset{bytes: bytes, info: info}
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
@ -381,6 +423,8 @@ var _bindata = map[string]func() (*asset, error){
|
|||||||
"000005_blocks_modifiedby.up.sql": _000005_blocks_modifiedbyUpSql,
|
"000005_blocks_modifiedby.up.sql": _000005_blocks_modifiedbyUpSql,
|
||||||
"000006_sharing_table.down.sql": _000006_sharing_tableDownSql,
|
"000006_sharing_table.down.sql": _000006_sharing_tableDownSql,
|
||||||
"000006_sharing_table.up.sql": _000006_sharing_tableUpSql,
|
"000006_sharing_table.up.sql": _000006_sharing_tableUpSql,
|
||||||
|
"000007_workspaces_table.down.sql": _000007_workspaces_tableDownSql,
|
||||||
|
"000007_workspaces_table.up.sql": _000007_workspaces_tableUpSql,
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssetDir returns the file names below a certain
|
// AssetDir returns the file names below a certain
|
||||||
@ -435,6 +479,8 @@ var _bintree = &bintree{nil, map[string]*bintree{
|
|||||||
"000005_blocks_modifiedby.up.sql": &bintree{_000005_blocks_modifiedbyUpSql, map[string]*bintree{}},
|
"000005_blocks_modifiedby.up.sql": &bintree{_000005_blocks_modifiedbyUpSql, map[string]*bintree{}},
|
||||||
"000006_sharing_table.down.sql": &bintree{_000006_sharing_tableDownSql, map[string]*bintree{}},
|
"000006_sharing_table.down.sql": &bintree{_000006_sharing_tableDownSql, map[string]*bintree{}},
|
||||||
"000006_sharing_table.up.sql": &bintree{_000006_sharing_tableUpSql, map[string]*bintree{}},
|
"000006_sharing_table.up.sql": &bintree{_000006_sharing_tableUpSql, map[string]*bintree{}},
|
||||||
|
"000007_workspaces_table.down.sql": &bintree{_000007_workspaces_tableDownSql, map[string]*bintree{}},
|
||||||
|
"000007_workspaces_table.up.sql": &bintree{_000007_workspaces_tableUpSql, map[string]*bintree{}},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// RestoreAsset restores an asset under the given directory
|
// RestoreAsset restores an asset under the given directory
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE workspaces;
|
@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS workspaces (
|
||||||
|
id VARCHAR(36),
|
||||||
|
signup_token VARCHAR(100) NOT NULL,
|
||||||
|
settings TEXT,
|
||||||
|
modified_by VARCHAR(36),
|
||||||
|
update_at BIGINT,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
@ -9,6 +9,22 @@ import (
|
|||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (s *SQLStore) GetActiveUserCount() (int, error) {
|
||||||
|
query := s.getQueryBuilder().
|
||||||
|
Select("count(*)").
|
||||||
|
From("users").
|
||||||
|
Where(sq.Eq{"delete_at": 0})
|
||||||
|
row := query.QueryRow()
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := row.Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) getUserByCondition(condition sq.Eq) (*model.User, error) {
|
func (s *SQLStore) getUserByCondition(condition sq.Eq) (*model.User, error) {
|
||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Select("id", "username", "email", "password", "mfa_secret", "auth_service", "auth_data", "props", "create_at", "update_at", "delete_at").
|
Select("id", "username", "email", "password", "mfa_secret", "auth_service", "auth_data", "props", "create_at", "update_at", "delete_at").
|
||||||
|
98
server/services/store/sqlstore/workspaces.go
Normal file
98
server/services/store/sqlstore/workspaces.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package sqlstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-octo-tasks/server/model"
|
||||||
|
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *SQLStore) UpsertWorkspaceSignupToken(workspace model.Workspace) error {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
query := s.getQueryBuilder().
|
||||||
|
Insert("workspaces").
|
||||||
|
Columns(
|
||||||
|
"id",
|
||||||
|
"signup_token",
|
||||||
|
"modified_by",
|
||||||
|
"update_at",
|
||||||
|
).
|
||||||
|
Values(
|
||||||
|
workspace.ID,
|
||||||
|
workspace.SignupToken,
|
||||||
|
workspace.ModifiedBy,
|
||||||
|
now,
|
||||||
|
).
|
||||||
|
Suffix("ON CONFLICT (id) DO UPDATE SET signup_token = EXCLUDED.signup_token, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at")
|
||||||
|
|
||||||
|
_, err := query.Exec()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) UpsertWorkspaceSettings(workspace model.Workspace) error {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
settingsJSON, err := json.Marshal(workspace.Settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := s.getQueryBuilder().
|
||||||
|
Insert("workspaces").
|
||||||
|
Columns(
|
||||||
|
"id",
|
||||||
|
"settings",
|
||||||
|
"modified_by",
|
||||||
|
"update_at",
|
||||||
|
).
|
||||||
|
Values(
|
||||||
|
workspace.ID,
|
||||||
|
settingsJSON,
|
||||||
|
workspace.ModifiedBy,
|
||||||
|
now,
|
||||||
|
).
|
||||||
|
Suffix("ON CONFLICT (id) DO UPDATE SET settings = EXCLUDED.settings, modified_by = EXCLUDED.modified_by, update_at = EXCLUDED.update_at")
|
||||||
|
|
||||||
|
_, err = query.Exec()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) GetWorkspace(ID string) (*model.Workspace, error) {
|
||||||
|
var settingsJSON string
|
||||||
|
|
||||||
|
query := s.getQueryBuilder().
|
||||||
|
Select(
|
||||||
|
"id",
|
||||||
|
"signup_token",
|
||||||
|
"COALESCE(\"settings\", '{}')",
|
||||||
|
"modified_by",
|
||||||
|
"update_at",
|
||||||
|
).
|
||||||
|
From("workspaces").
|
||||||
|
Where(sq.Eq{"id": ID})
|
||||||
|
row := query.QueryRow()
|
||||||
|
workspace := model.Workspace{}
|
||||||
|
|
||||||
|
err := row.Scan(
|
||||||
|
&workspace.ID,
|
||||||
|
&workspace.SignupToken,
|
||||||
|
&settingsJSON,
|
||||||
|
&workspace.ModifiedBy,
|
||||||
|
&workspace.UpdateAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(settingsJSON), &workspace.Settings)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(`ERROR GetWorkspace settings json.Unmarshal: %v`, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &workspace, nil
|
||||||
|
}
|
@ -21,6 +21,7 @@ type Store interface {
|
|||||||
GetSystemSettings() (map[string]string, error)
|
GetSystemSettings() (map[string]string, error)
|
||||||
SetSystemSetting(key string, value string) error
|
SetSystemSetting(key string, value string) error
|
||||||
|
|
||||||
|
GetActiveUserCount() (int, error)
|
||||||
GetUserById(userID string) (*model.User, error)
|
GetUserById(userID string) (*model.User, error)
|
||||||
GetUserByEmail(email string) (*model.User, error)
|
GetUserByEmail(email string) (*model.User, error)
|
||||||
GetUserByUsername(username string) (*model.User, error)
|
GetUserByUsername(username string) (*model.User, error)
|
||||||
@ -36,4 +37,8 @@ type Store interface {
|
|||||||
|
|
||||||
UpsertSharing(sharing model.Sharing) error
|
UpsertSharing(sharing model.Sharing) error
|
||||||
GetSharing(rootID string) (*model.Sharing, error)
|
GetSharing(rootID string) (*model.Sharing, error)
|
||||||
|
|
||||||
|
UpsertWorkspaceSignupToken(workspace model.Workspace) error
|
||||||
|
UpsertWorkspaceSettings(workspace model.Workspace) error
|
||||||
|
GetWorkspace(ID string) (*model.Workspace, error)
|
||||||
}
|
}
|
||||||
|
11
webapp/src/blocks/workspace.ts
Normal file
11
webapp/src/blocks/workspace.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
interface IWorkspace {
|
||||||
|
readonly id: string,
|
||||||
|
readonly signupToken: string,
|
||||||
|
readonly settings: Readonly<Record<string, any>>
|
||||||
|
readonly modifiedBy?: string,
|
||||||
|
readonly updateAt?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export {IWorkspace}
|
@ -18,6 +18,14 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media not screen and (max-width: 430px) {
|
||||||
|
&.top {
|
||||||
|
top: auto;
|
||||||
|
bottom: 25px;
|
||||||
|
left: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hideOnWidescreen {
|
.hideOnWidescreen {
|
||||||
/* Hide controls (e.g. close button) on larger screens */
|
/* Hide controls (e.g. close button) on larger screens */
|
||||||
@media not screen and (max-width: 430px) {
|
@media not screen and (max-width: 430px) {
|
||||||
|
@ -10,6 +10,7 @@ import './modal.scss'
|
|||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
intl: IntlShape
|
intl: IntlShape
|
||||||
|
position?: 'top'|'bottom'
|
||||||
}
|
}
|
||||||
|
|
||||||
class Modal extends React.PureComponent<Props> {
|
class Modal extends React.PureComponent<Props> {
|
||||||
@ -37,9 +38,11 @@ class Modal extends React.PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
|
const {position} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='Modal'
|
className={'Modal ' + (position || 'bottom')}
|
||||||
ref={this.node}
|
ref={this.node}
|
||||||
>
|
>
|
||||||
<div className='toolbar hideOnWidescreen'>
|
<div className='toolbar hideOnWidescreen'>
|
||||||
|
28
webapp/src/components/registrationLinkComponent.scss
Normal file
28
webapp/src/components/registrationLinkComponent.scss
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.RegistrationLinkComponent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5px;
|
||||||
|
color: rgb(var(--main-fg));
|
||||||
|
|
||||||
|
> .row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.shareUrl {
|
||||||
|
flex-grow: 1;
|
||||||
|
border: solid 1px #cccccc;
|
||||||
|
margin-right: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
92
webapp/src/components/registrationLinkComponent.tsx
Normal file
92
webapp/src/components/registrationLinkComponent.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import React from 'react'
|
||||||
|
import {injectIntl, IntlShape} from 'react-intl'
|
||||||
|
|
||||||
|
import {IWorkspace} from '../blocks/workspace'
|
||||||
|
import {sendFlashMessage} from '../components/flashMessages'
|
||||||
|
import client from '../octoClient'
|
||||||
|
import {Utils} from '../utils'
|
||||||
|
import Button from '../widgets/buttons/button'
|
||||||
|
|
||||||
|
import Modal from './modal'
|
||||||
|
import './registrationLinkComponent.scss'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void
|
||||||
|
intl: IntlShape
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
workspace?: IWorkspace
|
||||||
|
wasCopied?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegistrationLinkComponent extends React.PureComponent<Props, State> {
|
||||||
|
state: State = {}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadData() {
|
||||||
|
const workspace = await client.getWorkspace()
|
||||||
|
this.setState({workspace})
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
const {intl} = this.props
|
||||||
|
const {workspace} = this.state
|
||||||
|
|
||||||
|
const registrationUrl = window.location.origin + '/register?t=' + workspace?.signupToken
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
position='top'
|
||||||
|
onClose={this.props.onClose}
|
||||||
|
>
|
||||||
|
<div className='RegistrationLinkComponent'>
|
||||||
|
{workspace && <>
|
||||||
|
<div className='row'>
|
||||||
|
<input
|
||||||
|
key={registrationUrl}
|
||||||
|
className='shareUrl'
|
||||||
|
readOnly={true}
|
||||||
|
value={registrationUrl}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
filled={true}
|
||||||
|
onClick={() => {
|
||||||
|
Utils.copyTextToClipboard(registrationUrl)
|
||||||
|
this.setState({wasCopied: true})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{this.state.wasCopied ? intl.formatMessage({id: 'RegistrationLink.copiedLink', defaultMessage: 'Copied!'}) : intl.formatMessage({id: 'RegistrationLink.copyLink', defaultMessage: 'Copy link'})}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<Button onClick={this.onRegenerateToken}>
|
||||||
|
{intl.formatMessage({id: 'RegistrationLink.regenerateToken', defaultMessage: 'Regenerate token'})}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRegenerateToken = async () => {
|
||||||
|
const {intl} = this.props
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
const accept = window.confirm(intl.formatMessage({id: 'RegistrationLink.confirmRegenerateToken', defaultMessage: 'This will invalidate previously shared links. Continue?'}))
|
||||||
|
if (accept) {
|
||||||
|
await client.regenerateWorkspaceSignupToken()
|
||||||
|
await this.loadData()
|
||||||
|
|
||||||
|
const description = intl.formatMessage({id: 'RegistrationLink.tokenRegenerated', defaultMessage: 'Registration link regenerated'})
|
||||||
|
sendFlashMessage({content: description, severity: 'low'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(RegistrationLinkComponent)
|
@ -2,6 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
color: rgb(var(--main-fg));
|
||||||
|
|
||||||
> .row {
|
> .row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -7,7 +7,7 @@ import {Archiver} from '../archiver'
|
|||||||
import {Board, MutableBoard} from '../blocks/board'
|
import {Board, MutableBoard} from '../blocks/board'
|
||||||
import {BoardView, MutableBoardView} from '../blocks/boardView'
|
import {BoardView, MutableBoardView} from '../blocks/boardView'
|
||||||
import mutator from '../mutator'
|
import mutator from '../mutator'
|
||||||
import {defaultTheme, darkTheme, lightTheme, setTheme} from '../theme'
|
import {darkTheme, defaultTheme, lightTheme, setTheme} from '../theme'
|
||||||
import {WorkspaceTree} from '../viewModel/workspaceTree'
|
import {WorkspaceTree} from '../viewModel/workspaceTree'
|
||||||
import Button from '../widgets/buttons/button'
|
import Button from '../widgets/buttons/button'
|
||||||
import IconButton from '../widgets/buttons/iconButton'
|
import IconButton from '../widgets/buttons/iconButton'
|
||||||
@ -22,6 +22,9 @@ import OptionsIcon from '../widgets/icons/options'
|
|||||||
import ShowSidebarIcon from '../widgets/icons/showSidebar'
|
import ShowSidebarIcon from '../widgets/icons/showSidebar'
|
||||||
import Menu from '../widgets/menu'
|
import Menu from '../widgets/menu'
|
||||||
import MenuWrapper from '../widgets/menuWrapper'
|
import MenuWrapper from '../widgets/menuWrapper'
|
||||||
|
|
||||||
|
import ModalWrapper from './modalWrapper'
|
||||||
|
import RegistrationLinkComponent from './registrationLinkComponent'
|
||||||
import './sidebar.scss'
|
import './sidebar.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -36,6 +39,7 @@ type Props = {
|
|||||||
type State = {
|
type State = {
|
||||||
isHidden: boolean
|
isHidden: boolean
|
||||||
collapsedBoards: {[key: string]: boolean}
|
collapsedBoards: {[key: string]: boolean}
|
||||||
|
showRegistrationLinkDialog?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class Sidebar extends React.Component<Props, State> {
|
class Sidebar extends React.Component<Props, State> {
|
||||||
@ -263,63 +267,79 @@ class Sidebar extends React.Component<Props, State> {
|
|||||||
</Menu>
|
</Menu>
|
||||||
</MenuWrapper>
|
</MenuWrapper>
|
||||||
|
|
||||||
<MenuWrapper>
|
<ModalWrapper>
|
||||||
<Button>
|
<MenuWrapper>
|
||||||
<FormattedMessage
|
<Button>
|
||||||
id='Sidebar.settings'
|
<FormattedMessage
|
||||||
defaultMessage='Settings'
|
id='Sidebar.settings'
|
||||||
|
defaultMessage='Settings'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Menu position='top'>
|
||||||
|
<Menu.Text
|
||||||
|
id='invite'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.invite-users', defaultMessage: 'Invite Users'})}
|
||||||
|
onClick={async () => {
|
||||||
|
this.setState({showRegistrationLinkDialog: true})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Menu.Text
|
||||||
|
id='import'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.import-archive', defaultMessage: 'Import archive'})}
|
||||||
|
onClick={async () => Archiver.importFullArchive()}
|
||||||
|
/>
|
||||||
|
<Menu.Text
|
||||||
|
id='export'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.export-archive', defaultMessage: 'Export archive'})}
|
||||||
|
onClick={async () => Archiver.exportFullArchive()}
|
||||||
|
/>
|
||||||
|
<Menu.SubMenu
|
||||||
|
id='lang'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.set-language', defaultMessage: 'Set language'})}
|
||||||
|
position='top'
|
||||||
|
>
|
||||||
|
<Menu.Text
|
||||||
|
id='english-lang'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.english', defaultMessage: 'English'})}
|
||||||
|
onClick={async () => this.props.setLanguage('en')}
|
||||||
|
/>
|
||||||
|
<Menu.Text
|
||||||
|
id='spanish-lang'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.spanish', defaultMessage: 'Spanish'})}
|
||||||
|
onClick={async () => this.props.setLanguage('es')}
|
||||||
|
/>
|
||||||
|
</Menu.SubMenu>
|
||||||
|
<Menu.SubMenu
|
||||||
|
id='theme'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.set-theme', defaultMessage: 'Set theme'})}
|
||||||
|
position='top'
|
||||||
|
>
|
||||||
|
<Menu.Text
|
||||||
|
id='default-theme'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.default-theme', defaultMessage: 'Default theme'})}
|
||||||
|
onClick={async () => setTheme(defaultTheme)}
|
||||||
|
/>
|
||||||
|
<Menu.Text
|
||||||
|
id='dark-theme'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.dark-theme', defaultMessage: 'Dark theme'})}
|
||||||
|
onClick={async () => setTheme(darkTheme)}
|
||||||
|
/>
|
||||||
|
<Menu.Text
|
||||||
|
id='light-theme'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.light-theme', defaultMessage: 'Light theme'})}
|
||||||
|
onClick={async () => setTheme(lightTheme)}
|
||||||
|
/>
|
||||||
|
</Menu.SubMenu>
|
||||||
|
</Menu>
|
||||||
|
</MenuWrapper>
|
||||||
|
{this.state.showRegistrationLinkDialog &&
|
||||||
|
<RegistrationLinkComponent
|
||||||
|
onClose={() => {
|
||||||
|
this.setState({showRegistrationLinkDialog: false})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Button>
|
}
|
||||||
<Menu position='top'>
|
</ModalWrapper>
|
||||||
<Menu.Text
|
|
||||||
id='import'
|
|
||||||
name={intl.formatMessage({id: 'Sidebar.import-archive', defaultMessage: 'Import archive'})}
|
|
||||||
onClick={async () => Archiver.importFullArchive()}
|
|
||||||
/>
|
|
||||||
<Menu.Text
|
|
||||||
id='export'
|
|
||||||
name={intl.formatMessage({id: 'Sidebar.export-archive', defaultMessage: 'Export archive'})}
|
|
||||||
onClick={async () => Archiver.exportFullArchive()}
|
|
||||||
/>
|
|
||||||
<Menu.SubMenu
|
|
||||||
id='lang'
|
|
||||||
name={intl.formatMessage({id: 'Sidebar.set-language', defaultMessage: 'Set language'})}
|
|
||||||
position='top'
|
|
||||||
>
|
|
||||||
<Menu.Text
|
|
||||||
id='english-lang'
|
|
||||||
name={intl.formatMessage({id: 'Sidebar.english', defaultMessage: 'English'})}
|
|
||||||
onClick={async () => this.props.setLanguage('en')}
|
|
||||||
/>
|
|
||||||
<Menu.Text
|
|
||||||
id='spanish-lang'
|
|
||||||
name={intl.formatMessage({id: 'Sidebar.spanish', defaultMessage: 'Spanish'})}
|
|
||||||
onClick={async () => this.props.setLanguage('es')}
|
|
||||||
/>
|
|
||||||
</Menu.SubMenu>
|
|
||||||
<Menu.SubMenu
|
|
||||||
id='theme'
|
|
||||||
name={intl.formatMessage({id: 'Sidebar.set-theme', defaultMessage: 'Set theme'})}
|
|
||||||
position='top'
|
|
||||||
>
|
|
||||||
<Menu.Text
|
|
||||||
id='default-theme'
|
|
||||||
name={intl.formatMessage({id: 'Sidebar.default-theme', defaultMessage: 'Default theme'})}
|
|
||||||
onClick={async () => setTheme(defaultTheme)}
|
|
||||||
/>
|
|
||||||
<Menu.Text
|
|
||||||
id='dark-theme'
|
|
||||||
name={intl.formatMessage({id: 'Sidebar.dark-theme', defaultMessage: 'Dark theme'})}
|
|
||||||
onClick={async () => setTheme(darkTheme)}
|
|
||||||
/>
|
|
||||||
<Menu.Text
|
|
||||||
id='light-theme'
|
|
||||||
name={intl.formatMessage({id: 'Sidebar.light-theme', defaultMessage: 'Light theme'})}
|
|
||||||
onClick={async () => setTheme(lightTheme)}
|
|
||||||
/>
|
|
||||||
</Menu.SubMenu>
|
|
||||||
</Menu>
|
|
||||||
</MenuWrapper>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import {IBlock, IMutableBlock} from './blocks/block'
|
import {IBlock, IMutableBlock} from './blocks/block'
|
||||||
import {ISharing} from './blocks/sharing'
|
import {ISharing} from './blocks/sharing'
|
||||||
|
import {IWorkspace} from './blocks/workspace'
|
||||||
import {IUser} from './user'
|
import {IUser} from './user'
|
||||||
import {Utils} from './utils'
|
import {Utils} from './utils'
|
||||||
|
|
||||||
@ -43,18 +44,18 @@ class OctoClient {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(email: string, username: string, password: string): Promise<boolean> {
|
async register(email: string, username: string, password: string, token?: string): Promise<200 | 401 | 500> {
|
||||||
const path = '/api/v1/register'
|
const path = '/api/v1/register'
|
||||||
const body = JSON.stringify({email, username, password})
|
const body = JSON.stringify({email, username, password, token})
|
||||||
const response = await fetch(this.serverUrl + path, {
|
const response = await fetch(this.serverUrl + path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.headers(),
|
headers: this.headers(),
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
if (response.status === 200) {
|
if (response.status === 200 || response.status === 401) {
|
||||||
return true
|
return response.status
|
||||||
}
|
}
|
||||||
return false
|
return 500
|
||||||
}
|
}
|
||||||
|
|
||||||
private headers() {
|
private headers() {
|
||||||
@ -242,6 +243,28 @@ class OctoClient {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workspace
|
||||||
|
|
||||||
|
async getWorkspace(): Promise<IWorkspace> {
|
||||||
|
const path = '/api/v1/workspace'
|
||||||
|
const response = await fetch(this.serverUrl + path, {headers: this.headers()})
|
||||||
|
const workspace = (await response.json()) as IWorkspace || null
|
||||||
|
return workspace
|
||||||
|
}
|
||||||
|
|
||||||
|
async regenerateWorkspaceSignupToken(): Promise<boolean> {
|
||||||
|
const path = '/api/v1/workspace/regenerate_signup_token'
|
||||||
|
const response = await fetch(this.serverUrl + path, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.headers(),
|
||||||
|
})
|
||||||
|
if (response.status === 200) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReadToken(): string {
|
function getReadToken(): string {
|
||||||
|
@ -24,4 +24,7 @@
|
|||||||
.Button {
|
.Button {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
.error {
|
||||||
|
color: #900000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,22 +18,30 @@ type State = {
|
|||||||
email: string
|
email: string
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
|
errorMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class RegisterPage extends React.PureComponent<Props, State> {
|
class RegisterPage extends React.PureComponent<Props, State> {
|
||||||
state = {
|
state: State = {
|
||||||
email: '',
|
email: '',
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleRegister = async (): Promise<void> => {
|
private handleRegister = async (): Promise<void> => {
|
||||||
const registered = await client.register(this.state.email, this.state.username, this.state.password)
|
const queryString = new URLSearchParams(window.location.search)
|
||||||
if (registered) {
|
const signupToken = queryString.get('t') || ''
|
||||||
|
|
||||||
|
const registered = await client.register(this.state.email, this.state.username, this.state.password, signupToken)
|
||||||
|
if (registered === 200) {
|
||||||
const logged = await client.login(this.state.username, this.state.password)
|
const logged = await client.login(this.state.username, this.state.password)
|
||||||
if (logged) {
|
if (logged) {
|
||||||
this.props.history.push('/')
|
this.props.history.push('/')
|
||||||
}
|
}
|
||||||
|
} else if (registered === 401) {
|
||||||
|
this.setState({errorMessage: 'Invalid registration link, please contact your administrator'})
|
||||||
|
} else {
|
||||||
|
this.setState({errorMessage: 'Server error'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +75,11 @@ class RegisterPage extends React.PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<Button onClick={this.handleRegister}>{'Register'}</Button>
|
<Button onClick={this.handleRegister}>{'Register'}</Button>
|
||||||
<Link to='/login'>{'or login if you already have an account'}</Link>
|
<Link to='/login'>{'or login if you already have an account'}</Link>
|
||||||
|
{this.state.errorMessage &&
|
||||||
|
<div className='error'>
|
||||||
|
{this.state.errorMessage}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user