commit
4d199ba8af
27 changed files with 708 additions and 80 deletions
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/app"
|
||||
"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.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) {
|
||||
|
@ -427,7 +431,6 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
log.Printf(`ERROR: %v`, r)
|
||||
errorResponse(w, http.StatusInternalServerError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -435,7 +438,6 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
log.Printf(`ERROR: %v`, r)
|
||||
errorResponse(w, http.StatusInternalServerError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -447,7 +449,6 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
|
|||
requestBody, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -456,7 +457,6 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
|
|||
if r := recover(); r != nil {
|
||||
log.Printf(`ERROR: %v`, r)
|
||||
errorResponse(w, http.StatusInternalServerError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
@ -466,7 +466,6 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
|
|||
err = json.Unmarshal(requestBody, &sharing)
|
||||
if err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -483,7 +482,6 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
log.Printf(`ERROR: %v, REQUEST: %v`, err, r)
|
||||
errorResponse(w, http.StatusInternalServerError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -491,6 +489,46 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
|
|||
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
|
||||
|
||||
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -26,6 +26,7 @@ type RegisterData struct {
|
|||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
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"})
|
||||
return
|
||||
}
|
||||
|
||||
func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
requestBody, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -95,6 +94,33 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|||
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 {
|
||||
errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
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()})
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -27,6 +27,11 @@ func (a *App) GetSession(token string) (*model.Session, error) {
|
|||
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
|
||||
func (a *App) GetUser(ID string) (*model.User, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to start the database", 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) }
|
||||
api := api.NewAPI(appBuilder, singleUser)
|
||||
|
||||
// Init workspace
|
||||
appBuilder().GetRootWorkspace()
|
||||
|
||||
webServer := web.NewServer(cfg.WebPath, cfg.Port, cfg.UseSSL)
|
||||
webServer.AddRoutes(wsServer)
|
||||
webServer.AddRoutes(api)
|
||||
|
|
|
@ -103,6 +103,21 @@ func (mr *MockStoreMockRecorder) DeleteSession(arg0 interface{}) *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSession", reflect.TypeOf((*MockStore)(nil).DeleteSession), arg0)
|
||||
}
|
||||
|
||||
// GetActiveUserCount mocks base method
|
||||
func (m *MockStore) GetActiveUserCount() (int, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetActiveUserCount")
|
||||
ret0, _ := ret[0].(int)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetActiveUserCount indicates an expected call of GetActiveUserCount
|
||||
func (mr *MockStoreMockRecorder) GetActiveUserCount() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveUserCount", reflect.TypeOf((*MockStore)(nil).GetActiveUserCount))
|
||||
}
|
||||
|
||||
// GetAllBlocks mocks base method
|
||||
func (m *MockStore) GetAllBlocks() ([]model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -313,6 +328,21 @@ func (mr *MockStoreMockRecorder) GetUserByUsername(arg0 interface{}) *gomock.Cal
|
|||
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
|
||||
func (m *MockStore) InsertBlock(arg0 model.Block) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -410,3 +440,31 @@ func (mr *MockStoreMockRecorder) UpsertSharing(arg0 interface{}) *gomock.Call {
|
|||
mr.mock.ctrl.T.Helper()
|
||||
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/000006_sharing_table.down.sql
|
||||
// postgres_files/000006_sharing_table.up.sql
|
||||
// postgres_files/000007_workspaces_table.down.sql
|
||||
// postgres_files/000007_workspaces_table.up.sql
|
||||
package postgres
|
||||
|
||||
import (
|
||||
|
@ -292,7 +294,7 @@ func _000006_sharing_tableDownSql() (*asset, error) {
|
|||
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}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -312,7 +314,47 @@ func _000006_sharing_tableUpSql() (*asset, error) {
|
|||
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}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -381,6 +423,8 @@ var _bindata = map[string]func() (*asset, error){
|
|||
"000005_blocks_modifiedby.up.sql": _000005_blocks_modifiedbyUpSql,
|
||||
"000006_sharing_table.down.sql": _000006_sharing_tableDownSql,
|
||||
"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
|
||||
|
@ -435,6 +479,8 @@ var _bintree = &bintree{nil, 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.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
|
||||
|
|
|
@ -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/000006_sharing_table.down.sql
|
||||
// sqlite_files/000006_sharing_table.up.sql
|
||||
// sqlite_files/000007_workspaces_table.down.sql
|
||||
// sqlite_files/000007_workspaces_table.up.sql
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
|
@ -292,7 +294,7 @@ func _000006_sharing_tableDownSql() (*asset, error) {
|
|||
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}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -312,7 +314,47 @@ func _000006_sharing_tableUpSql() (*asset, error) {
|
|||
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}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -381,6 +423,8 @@ var _bindata = map[string]func() (*asset, error){
|
|||
"000005_blocks_modifiedby.up.sql": _000005_blocks_modifiedbyUpSql,
|
||||
"000006_sharing_table.down.sql": _000006_sharing_tableDownSql,
|
||||
"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
|
||||
|
@ -435,6 +479,8 @@ var _bintree = &bintree{nil, 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.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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
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) {
|
||||
query := s.getQueryBuilder().
|
||||
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)
|
||||
SetSystemSetting(key string, value string) error
|
||||
|
||||
GetActiveUserCount() (int, error)
|
||||
GetUserById(userID string) (*model.User, error)
|
||||
GetUserByEmail(email string) (*model.User, error)
|
||||
GetUserByUsername(username string) (*model.User, error)
|
||||
|
@ -36,4 +37,8 @@ type Store interface {
|
|||
|
||||
UpsertSharing(sharing 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)
|
||||
}
|
||||
|
|
|
@ -60,6 +60,11 @@
|
|||
"PropertyType.URL": "URL",
|
||||
"PropertyType.UpdatedBy": "Updated By",
|
||||
"PropertyType.UpdatedTime": "Updated Time",
|
||||
"RegistrationLink.confirmRegenerateToken": "This will invalidate previously shared links. Continue?",
|
||||
"RegistrationLink.copiedLink": "Copied!",
|
||||
"RegistrationLink.copyLink": "Copy link",
|
||||
"RegistrationLink.regenerateToken": "Regenerate token",
|
||||
"RegistrationLink.tokenRegenerated": "Registration link regenerated",
|
||||
"ShareBoard.confirmRegenerateToken": "This will invalidate previously shared links. Continue?",
|
||||
"ShareBoard.copiedLink": "Copied!",
|
||||
"ShareBoard.copyLink": "Copy link",
|
||||
|
@ -79,6 +84,7 @@
|
|||
"Sidebar.english": "English",
|
||||
"Sidebar.export-archive": "Export archive",
|
||||
"Sidebar.import-archive": "Import archive",
|
||||
"Sidebar.invite-users": "Invite Users",
|
||||
"Sidebar.light-theme": "Light theme",
|
||||
"Sidebar.no-views-in-board": "No pages inside",
|
||||
"Sidebar.select-a-template": "Select a template",
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
@media not screen and (max-width: 430px) {
|
||||
&.top {
|
||||
top: auto;
|
||||
bottom: 25px;
|
||||
left: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.hideOnWidescreen {
|
||||
/* Hide controls (e.g. close button) on larger screens */
|
||||
@media not screen and (max-width: 430px) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import './modal.scss'
|
|||
type Props = {
|
||||
onClose: () => void
|
||||
intl: IntlShape
|
||||
position?: 'top'|'bottom'
|
||||
}
|
||||
|
||||
class Modal extends React.PureComponent<Props> {
|
||||
|
@ -37,9 +38,11 @@ class Modal extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const {position} = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className='Modal'
|
||||
className={'Modal ' + (position || 'bottom')}
|
||||
ref={this.node}
|
||||
>
|
||||
<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;
|
||||
flex-direction: column;
|
||||
padding: 5px;
|
||||
color: rgb(var(--main-fg));
|
||||
|
||||
> .row {
|
||||
display: flex;
|
||||
|
|
|
@ -7,7 +7,7 @@ import {Archiver} from '../archiver'
|
|||
import {Board, MutableBoard} from '../blocks/board'
|
||||
import {BoardView, MutableBoardView} from '../blocks/boardView'
|
||||
import mutator from '../mutator'
|
||||
import {defaultTheme, darkTheme, lightTheme, setTheme} from '../theme'
|
||||
import {darkTheme, defaultTheme, lightTheme, setTheme} from '../theme'
|
||||
import {WorkspaceTree} from '../viewModel/workspaceTree'
|
||||
import Button from '../widgets/buttons/button'
|
||||
import IconButton from '../widgets/buttons/iconButton'
|
||||
|
@ -22,6 +22,9 @@ import OptionsIcon from '../widgets/icons/options'
|
|||
import ShowSidebarIcon from '../widgets/icons/showSidebar'
|
||||
import Menu from '../widgets/menu'
|
||||
import MenuWrapper from '../widgets/menuWrapper'
|
||||
|
||||
import ModalWrapper from './modalWrapper'
|
||||
import RegistrationLinkComponent from './registrationLinkComponent'
|
||||
import './sidebar.scss'
|
||||
|
||||
type Props = {
|
||||
|
@ -36,6 +39,7 @@ type Props = {
|
|||
type State = {
|
||||
isHidden: boolean
|
||||
collapsedBoards: {[key: string]: boolean}
|
||||
showRegistrationLinkDialog?: boolean
|
||||
}
|
||||
|
||||
class Sidebar extends React.Component<Props, State> {
|
||||
|
@ -263,63 +267,79 @@ class Sidebar extends React.Component<Props, State> {
|
|||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='Sidebar.settings'
|
||||
defaultMessage='Settings'
|
||||
<ModalWrapper>
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
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'>
|
||||
<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>
|
||||
}
|
||||
</ModalWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
import {IBlock, IMutableBlock} from './blocks/block'
|
||||
import {ISharing} from './blocks/sharing'
|
||||
import {IWorkspace} from './blocks/workspace'
|
||||
import {IUser} from './user'
|
||||
import {Utils} from './utils'
|
||||
|
||||
|
@ -43,18 +44,18 @@ class OctoClient {
|
|||
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 body = JSON.stringify({email, username, password})
|
||||
const body = JSON.stringify({email, username, password, token})
|
||||
const response = await fetch(this.serverUrl + path, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body,
|
||||
})
|
||||
if (response.status === 200) {
|
||||
return true
|
||||
if (response.status === 200 || response.status === 401) {
|
||||
return response.status
|
||||
}
|
||||
return false
|
||||
return 500
|
||||
}
|
||||
|
||||
private headers() {
|
||||
|
@ -242,6 +243,28 @@ class OctoClient {
|
|||
}
|
||||
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 {
|
||||
|
|
|
@ -24,4 +24,7 @@
|
|||
.Button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.error {
|
||||
color: #900000;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,22 +18,30 @@ type State = {
|
|||
email: string
|
||||
username: string
|
||||
password: string
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
class RegisterPage extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
state: State = {
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
}
|
||||
|
||||
private handleRegister = async (): Promise<void> => {
|
||||
const registered = await client.register(this.state.email, this.state.username, this.state.password)
|
||||
if (registered) {
|
||||
const queryString = new URLSearchParams(window.location.search)
|
||||
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)
|
||||
if (logged) {
|
||||
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>
|
||||
<Button onClick={this.handleRegister}>{'Register'}</Button>
|
||||
<Link to='/login'>{'or login if you already have an account'}</Link>
|
||||
{this.state.errorMessage &&
|
||||
<div className='error'>
|
||||
{this.state.errorMessage}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue