Merge branch 'main' into admin-local
This commit is contained in:
commit
6af0780c17
40 changed files with 574 additions and 150 deletions
7
.github/workflows/build-ubuntu.yml
vendored
7
.github/workflows/build-ubuntu.yml
vendored
|
@ -25,10 +25,13 @@ jobs:
|
|||
with:
|
||||
go-version: 1.15
|
||||
|
||||
- name: apt-get libgtk-3-dev
|
||||
- name: apt-get update
|
||||
run: sudo apt-get update
|
||||
|
||||
- name: apt-get install libgtk-3-dev
|
||||
run: sudo apt-get install libgtk-3-dev
|
||||
|
||||
- name: apt-get libwebkit2gtk-4.0-dev
|
||||
- name: apt-get install libwebkit2gtk-4.0-dev
|
||||
run: sudo apt-get install libwebkit2gtk-4.0-dev
|
||||
|
||||
- name: Build Linux server and app
|
||||
|
|
5
Makefile
5
Makefile
|
@ -119,8 +119,8 @@ mac-app: server-mac webapp
|
|||
|
||||
win-app: server-win webapp
|
||||
cd win; make build
|
||||
mkdir -p win/temp/bin
|
||||
cp -R bin/win/octoserver.exe win/temp/bin
|
||||
mkdir -p win/temp
|
||||
cp -R bin/win/octoserver.exe win/temp
|
||||
cp -R app-config.json win/temp/config.json
|
||||
cp -R webapp/pack win/temp/pack
|
||||
mkdir -p win/dist
|
||||
|
@ -130,6 +130,7 @@ win-app: server-win webapp
|
|||
linux-app: server-linux webapp
|
||||
rm -rf linux/temp
|
||||
mkdir -p linux/dist
|
||||
mkdir -p linux/temp/tasks-app
|
||||
cp -R bin/linux/octoserver linux/temp/tasks-app/
|
||||
cp -R app-config.json linux/temp/tasks-app/config.json
|
||||
cp -R webapp/pack linux/temp/tasks-app/pack
|
||||
|
|
|
@ -42,6 +42,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
|||
|
||||
r.HandleFunc("/api/v1/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET")
|
||||
r.HandleFunc("/api/v1/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET")
|
||||
r.HandleFunc("/api/v1/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST")
|
||||
|
||||
r.HandleFunc("/api/v1/login", a.handleLogin).Methods("POST")
|
||||
r.HandleFunc("/api/v1/register", a.handleRegister).Methods("POST")
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/model"
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/services/auth"
|
||||
)
|
||||
|
@ -39,9 +40,38 @@ func (rd *RegisterData) IsValid() error {
|
|||
if !strings.Contains(rd.Email, "@") {
|
||||
return errors.New("Invalid email format")
|
||||
}
|
||||
if !strings.Contains(rd.Password, "") {
|
||||
if rd.Password == "" {
|
||||
return errors.New("Password is required")
|
||||
}
|
||||
if err := isValidPassword(rd.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ChangePasswordData struct {
|
||||
OldPassword string `json:"oldPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
func (rd *ChangePasswordData) IsValid() error {
|
||||
if rd.OldPassword == "" {
|
||||
return errors.New("Old password is required")
|
||||
}
|
||||
if rd.NewPassword == "" {
|
||||
return errors.New("New password is required")
|
||||
}
|
||||
if err := isValidPassword(rd.NewPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidPassword(password string) error {
|
||||
if len(password) < 8 {
|
||||
return errors.New("Password must be at least 8 characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -131,6 +161,35 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|||
jsonBytesResponse(w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["userID"]
|
||||
|
||||
requestBody, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
var requestData ChangePasswordData
|
||||
if err := json.Unmarshal(requestBody, &requestData); err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = requestData.IsValid(); err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = a.app().ChangePassword(userID, requestData.OldPassword, requestData.NewPassword); err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonBytesResponse(w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func (a *API) sessionRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
|
||||
return a.attachSession(handler, true)
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ func (a *App) Login(username string, email string, password string, mfaToken str
|
|||
}
|
||||
|
||||
if !auth.ComparePassword(user.Password, password) {
|
||||
log.Printf("Not valid passowrd. %s (%s)\n", password, user.Password)
|
||||
log.Printf("Invalid password for userID: %s\n", user.ID)
|
||||
return "", errors.New("invalid username or password")
|
||||
}
|
||||
|
||||
|
@ -141,3 +141,30 @@ func (a *App) UpdateUserPassword(username string, password string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ChangePassword(userID string, oldPassword string, newPassword string) error {
|
||||
var user *model.User
|
||||
if userID != "" {
|
||||
var err error
|
||||
user, err = a.store.GetUserById(userID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid username or password")
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return errors.New("invalid username or password")
|
||||
}
|
||||
|
||||
if !auth.ComparePassword(user.Password, oldPassword) {
|
||||
log.Printf("Invalid password for userID: %s\n", user.ID)
|
||||
return errors.New("invalid username or password")
|
||||
}
|
||||
|
||||
err := a.store.UpdateUserPasswordByID(userID, auth.HashPassword(newPassword))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to update password")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -441,6 +441,20 @@ func (mr *MockStoreMockRecorder) UpdateUserPassword(arg0, arg1 interface{}) *gom
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPassword", reflect.TypeOf((*MockStore)(nil).UpdateUserPassword), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateUserPasswordByID mocks base method
|
||||
func (m *MockStore) UpdateUserPasswordByID(arg0, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateUserPasswordByID", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateUserPasswordByID indicates an expected call of UpdateUserPasswordByID
|
||||
func (mr *MockStoreMockRecorder) UpdateUserPasswordByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPasswordByID", reflect.TypeOf((*MockStore)(nil).UpdateUserPasswordByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpsertSharing mocks base method
|
||||
func (m *MockStore) UpsertSharing(arg0 model.Sharing) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -106,3 +106,15 @@ func (s *SQLStore) UpdateUserPassword(username string, password string) error {
|
|||
_, err := query.Exec()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SQLStore) UpdateUserPasswordByID(userID string, password string) error {
|
||||
now := time.Now().Unix()
|
||||
|
||||
query := s.getQueryBuilder().Update("users").
|
||||
Set("password", password).
|
||||
Set("update_at", now).
|
||||
Where(sq.Eq{"id": userID})
|
||||
|
||||
_, err := query.Exec()
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ type Store interface {
|
|||
CreateUser(user *model.User) error
|
||||
UpdateUser(user *model.User) error
|
||||
UpdateUserPassword(username string, password string) error
|
||||
UpdateUserPasswordByID(userID string, password string) error
|
||||
|
||||
GetSession(token string, expireTime int64) (*model.Session, error)
|
||||
CreateSession(session *model.Session) error
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"BoardComponent.no-property": "No {property}",
|
||||
"BoardComponent.no-property-title": "Items with an empty {property} property will go here. This column cannot be removed.",
|
||||
"BoardComponent.show": "Show",
|
||||
"BoardPage.syncFailed": "Board may be deleted or access revoked.",
|
||||
"CardDetail.add-content": "Add content",
|
||||
"CardDetail.add-icon": "Add icon",
|
||||
"CardDetail.add-property": "+ Add a property",
|
||||
|
@ -63,6 +64,7 @@
|
|||
"RegistrationLink.confirmRegenerateToken": "This will invalidate previously shared links. Continue?",
|
||||
"RegistrationLink.copiedLink": "Copied!",
|
||||
"RegistrationLink.copyLink": "Copy link",
|
||||
"RegistrationLink.description": "Share this link for others to create accounts:",
|
||||
"RegistrationLink.regenerateToken": "Regenerate token",
|
||||
"RegistrationLink.tokenRegenerated": "Registration link regenerated",
|
||||
"ShareBoard.confirmRegenerateToken": "This will invalidate previously shared links. Continue?",
|
||||
|
@ -74,6 +76,7 @@
|
|||
"ShareBoard.unshare": "Anyone with the link can view this board",
|
||||
"Sidebar.add-board": "+ Add Board",
|
||||
"Sidebar.add-template": "+ New template",
|
||||
"Sidebar.changePassword": "Change password",
|
||||
"Sidebar.dark-theme": "Dark theme",
|
||||
"Sidebar.default-theme": "Default theme",
|
||||
"Sidebar.delete-board": "Delete board",
|
||||
|
@ -86,6 +89,7 @@
|
|||
"Sidebar.import-archive": "Import archive",
|
||||
"Sidebar.invite-users": "Invite Users",
|
||||
"Sidebar.light-theme": "Light theme",
|
||||
"Sidebar.logout": "Log out",
|
||||
"Sidebar.no-views-in-board": "No pages inside",
|
||||
"Sidebar.select-a-template": "Select a template",
|
||||
"Sidebar.set-language": "Set language",
|
||||
|
@ -93,6 +97,7 @@
|
|||
"Sidebar.settings": "Settings",
|
||||
"Sidebar.spanish": "Spanish",
|
||||
"Sidebar.template-from-board": "New template from board",
|
||||
"Sidebar.title": "Boards",
|
||||
"Sidebar.untitled": "Untitled",
|
||||
"Sidebar.untitled-board": "(Untitled Board)",
|
||||
"Sidebar.untitled-view": "(Untitled View)",
|
||||
|
|
|
@ -2,24 +2,21 @@
|
|||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {IntlProvider} from 'react-intl'
|
||||
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
Route,
|
||||
Switch,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import client from './octoClient'
|
||||
import {IUser, UserContext} from './user'
|
||||
|
||||
import {getCurrentLanguage, getMessages, storeLanguage} from './i18n'
|
||||
|
||||
import {FlashMessages} from './components/flashMessages'
|
||||
|
||||
import {getCurrentLanguage, getMessages, storeLanguage} from './i18n'
|
||||
import client from './octoClient'
|
||||
import BoardPage from './pages/boardPage'
|
||||
import ChangePasswordPage from './pages/changePasswordPage'
|
||||
import LoginPage from './pages/loginPage'
|
||||
import RegisterPage from './pages/registerPage'
|
||||
import BoardPage from './pages/boardPage'
|
||||
import {IUser, UserContext} from './user'
|
||||
|
||||
type State = {
|
||||
language: string,
|
||||
|
@ -65,6 +62,9 @@ export default class App extends React.PureComponent<unknown, State> {
|
|||
<Route path='/register'>
|
||||
<RegisterPage/>
|
||||
</Route>
|
||||
<Route path='/change_password'>
|
||||
<ChangePasswordPage/>
|
||||
</Route>
|
||||
<Route path='/shared'>
|
||||
<BoardPage
|
||||
readonly={true}
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
bottom: 25px;
|
||||
left: 25px;
|
||||
}
|
||||
&.bottom-right {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hideOnWidescreen {
|
||||
|
|
|
@ -10,7 +10,7 @@ import './modal.scss'
|
|||
type Props = {
|
||||
onClose: () => void
|
||||
intl: IntlShape
|
||||
position?: 'top'|'bottom'
|
||||
position?: 'top'|'bottom'|'bottom-right'
|
||||
}
|
||||
|
||||
class Modal extends React.PureComponent<Props> {
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
flex-direction: column;
|
||||
padding: 5px;
|
||||
color: rgb(var(--main-fg));
|
||||
font-weight: normal;
|
||||
line-height: normal;
|
||||
|
||||
> .row {
|
||||
display: flex;
|
||||
|
|
|
@ -42,7 +42,7 @@ class RegistrationLinkComponent extends React.PureComponent<Props, State> {
|
|||
|
||||
return (
|
||||
<Modal
|
||||
position='top'
|
||||
position='bottom-right'
|
||||
onClose={this.props.onClose}
|
||||
>
|
||||
<div className='RegistrationLinkComponent'>
|
||||
|
|
|
@ -44,8 +44,9 @@
|
|||
padding: 3px 20px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
>.username {
|
||||
>.heading {
|
||||
line-height: 30px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
>.IconButton {
|
||||
|
|
|
@ -9,7 +9,7 @@ import {BoardView, MutableBoardView} from '../blocks/boardView'
|
|||
import mutator from '../mutator'
|
||||
import octoClient from '../octoClient'
|
||||
import {darkTheme, defaultTheme, lightTheme, setTheme} from '../theme'
|
||||
import {UserContext} from '../user'
|
||||
import {IUser, UserContext} from '../user'
|
||||
import {WorkspaceTree} from '../viewModel/workspaceTree'
|
||||
import Button from '../widgets/buttons/button'
|
||||
import IconButton from '../widgets/buttons/iconButton'
|
||||
|
@ -87,28 +87,21 @@ class Sidebar extends React.Component<Props, State> {
|
|||
return (
|
||||
<div className='Sidebar octo-sidebar'>
|
||||
<div className='octo-sidebar-header'>
|
||||
<UserContext.Consumer>
|
||||
{(user) => (
|
||||
<div className='username'>
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
{user?.username}
|
||||
</Button>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
id='logout'
|
||||
name={intl.formatMessage({id: 'Sidebar.logout', defaultMessage: 'Log out'})}
|
||||
onClick={async () => {
|
||||
octoClient.logout()
|
||||
window.location.href = '/login'
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</UserContext.Consumer>
|
||||
<div className='heading'>
|
||||
<UserContext.Consumer>
|
||||
{(user) => {
|
||||
if (user) {
|
||||
if (user.id === 'single-user') {
|
||||
return (
|
||||
<div>{intl.formatMessage({id: 'Sidebar.title', defaultMessage: 'Boards'})}</div>
|
||||
)
|
||||
}
|
||||
return this.renderUserMenu(user)
|
||||
}
|
||||
return <div/>
|
||||
}}
|
||||
</UserContext.Consumer>
|
||||
</div>
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
<IconButton
|
||||
|
@ -291,80 +284,110 @@ class Sidebar extends React.Component<Props, State> {
|
|||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
||||
<ModalWrapper>
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='Sidebar.settings'
|
||||
defaultMessage='Settings'
|
||||
/>
|
||||
</Button>
|
||||
<Menu position='top'>
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
<FormattedMessage
|
||||
id='Sidebar.settings'
|
||||
defaultMessage='Settings'
|
||||
/>
|
||||
</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='invite'
|
||||
name={intl.formatMessage({id: 'Sidebar.invite-users', defaultMessage: 'Invite Users'})}
|
||||
onClick={async () => {
|
||||
this.setState({showRegistrationLinkDialog: true})
|
||||
}}
|
||||
id='english-lang'
|
||||
name={intl.formatMessage({id: 'Sidebar.english', defaultMessage: 'English'})}
|
||||
onClick={async () => this.props.setLanguage('en')}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='import'
|
||||
name={intl.formatMessage({id: 'Sidebar.import-archive', defaultMessage: 'Import archive'})}
|
||||
onClick={async () => Archiver.importFullArchive()}
|
||||
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='export'
|
||||
name={intl.formatMessage({id: 'Sidebar.export-archive', defaultMessage: 'Export archive'})}
|
||||
onClick={async () => Archiver.exportFullArchive()}
|
||||
id='dark-theme'
|
||||
name={intl.formatMessage({id: 'Sidebar.dark-theme', defaultMessage: 'Dark theme'})}
|
||||
onClick={async () => setTheme(darkTheme)}
|
||||
/>
|
||||
<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})
|
||||
<Menu.Text
|
||||
id='light-theme'
|
||||
name={intl.formatMessage({id: 'Sidebar.light-theme', defaultMessage: 'Light theme'})}
|
||||
onClick={async () => setTheme(lightTheme)}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private renderUserMenu(user: IUser): JSX.Element {
|
||||
const {intl} = this.props
|
||||
|
||||
return (
|
||||
<ModalWrapper>
|
||||
<MenuWrapper>
|
||||
<Button>
|
||||
{user.username}
|
||||
</Button>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
id='logout'
|
||||
name={intl.formatMessage({id: 'Sidebar.logout', defaultMessage: 'Log out'})}
|
||||
onClick={async () => {
|
||||
octoClient.logout()
|
||||
window.location.href = '/login'
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</ModalWrapper>
|
||||
</div>
|
||||
<Menu.Text
|
||||
id='changePassword'
|
||||
name={intl.formatMessage({id: 'Sidebar.changePassword', defaultMessage: 'Change password'})}
|
||||
onClick={async () => {
|
||||
window.location.href = '/change_password'
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='invite'
|
||||
name={intl.formatMessage({id: 'Sidebar.invite-users', defaultMessage: 'Invite Users'})}
|
||||
onClick={async () => {
|
||||
this.setState({showRegistrationLinkDialog: true})
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
||||
{this.state.showRegistrationLinkDialog &&
|
||||
<RegistrationLinkComponent
|
||||
onClose={() => {
|
||||
this.setState({showRegistrationLinkDialog: false})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -299,11 +299,11 @@ class ViewHeader extends React.Component<Props, State> {
|
|||
name={intl.formatMessage({id: 'ViewHeader.export-csv', defaultMessage: 'Export to CSV'})}
|
||||
onClick={() => CsvExporter.exportTableCsv(boardTree)}
|
||||
/>
|
||||
<Menu.Text
|
||||
{/* <Menu.Text
|
||||
id='exportBoardArchive'
|
||||
name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export board archive'})}
|
||||
onClick={() => Archiver.exportBoardTree(boardTree)}
|
||||
/>
|
||||
/> */}
|
||||
<Menu.Text
|
||||
id='shareBoard'
|
||||
name={intl.formatMessage({id: 'ViewHeader.share-board', defaultMessage: 'Share board'})}
|
||||
|
|
|
@ -36,6 +36,10 @@ class CsvExporter {
|
|||
// TODO: Remove or reuse link
|
||||
}
|
||||
|
||||
private static encodeText(text: string): string {
|
||||
return text.replace(/"/g, '""')
|
||||
}
|
||||
|
||||
private static generateTableArray(boardTree: BoardTree, viewToExport: BoardView): string[][] {
|
||||
const {board, cards} = boardTree
|
||||
|
||||
|
@ -44,7 +48,7 @@ class CsvExporter {
|
|||
|
||||
{
|
||||
// Header row
|
||||
const row: string[] = []
|
||||
const row: string[] = ['Title']
|
||||
visibleProperties.forEach((template) => {
|
||||
row.push(template.name)
|
||||
})
|
||||
|
@ -53,6 +57,7 @@ class CsvExporter {
|
|||
|
||||
cards.forEach((card) => {
|
||||
const row: string[] = []
|
||||
row.push(`"${this.encodeText(card.title)}"`)
|
||||
visibleProperties.forEach((template) => {
|
||||
const propertyValue = card.properties[template.id]
|
||||
const displayValue = OctoUtils.propertyDisplayValue(card, propertyValue, template) || ''
|
||||
|
@ -61,7 +66,7 @@ class CsvExporter {
|
|||
row.push(numericValue)
|
||||
} else {
|
||||
// Export as string
|
||||
row.push(`"${displayValue}"`)
|
||||
row.push(`"${this.encodeText(displayValue)}"`)
|
||||
}
|
||||
})
|
||||
rows.push(row)
|
||||
|
|
|
@ -70,6 +70,18 @@ class OctoClient {
|
|||
return {code: response.status, json}
|
||||
}
|
||||
|
||||
async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<{code: number, json: any}> {
|
||||
const path = `/api/v1/users/${encodeURIComponent(userId)}/changepassword`
|
||||
const body = JSON.stringify({oldPassword, newPassword})
|
||||
const response = await fetch(this.serverUrl + path, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body,
|
||||
})
|
||||
const json = (await this.getJson(response))
|
||||
return {code: response.status, json}
|
||||
}
|
||||
|
||||
private headers() {
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
|
|
64
webapp/src/pages/changePasswordPage.scss
Normal file
64
webapp/src/pages/changePasswordPage.scss
Normal file
|
@ -0,0 +1,64 @@
|
|||
.ChangePasswordPage {
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 15px;
|
||||
width: 450px;
|
||||
height: 400px;
|
||||
margin: 150px auto;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
box-shadow: rgba(var(--main-fg), 0.1) 0px 0px 0px 1px, rgba(var(--main-fg), 0.3) 0px 4px 8px;
|
||||
|
||||
@media screen and (max-width: 430px) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.oldPassword, .newPassword {
|
||||
margin-bottom: 10px;
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
input {
|
||||
display: inline-block;
|
||||
width: 250px;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 4px;
|
||||
padding: 7px;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
> .Button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
min-height: 38px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #900000;
|
||||
}
|
||||
|
||||
.succeeded {
|
||||
background-color: #ccffcc;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
104
webapp/src/pages/changePasswordPage.tsx
Normal file
104
webapp/src/pages/changePasswordPage.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {
|
||||
withRouter,
|
||||
RouteComponentProps,
|
||||
Link,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import Button from '../widgets/buttons/button'
|
||||
import client from '../octoClient'
|
||||
import './changePasswordPage.scss'
|
||||
import {UserContext} from '../user'
|
||||
|
||||
type Props = RouteComponentProps
|
||||
|
||||
type State = {
|
||||
oldPassword: string
|
||||
newPassword: string
|
||||
errorMessage?: string
|
||||
succeeded: boolean
|
||||
}
|
||||
|
||||
class ChangePasswordPage extends React.PureComponent<Props, State> {
|
||||
state: State = {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
succeeded: false,
|
||||
}
|
||||
|
||||
private handleSubmit = async (userId: string): Promise<void> => {
|
||||
const response = await client.changePassword(userId, this.state.oldPassword, this.state.newPassword)
|
||||
if (response.code === 200) {
|
||||
this.setState({succeeded: true})
|
||||
} else {
|
||||
this.setState({errorMessage: `Change password failed: ${response.json?.error}`})
|
||||
}
|
||||
}
|
||||
|
||||
private closeClicked = () => {
|
||||
this.props.history.push('/')
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<div className='ChangePasswordPage'>
|
||||
<div className='title'>{'Change Password'}</div>
|
||||
|
||||
<UserContext.Consumer>
|
||||
{(user) => {
|
||||
if (user) {
|
||||
return (<>
|
||||
<div className='oldPassword'>
|
||||
<input
|
||||
id='login-oldpassword'
|
||||
type='password'
|
||||
placeholder={'Enter current password'}
|
||||
value={this.state.oldPassword}
|
||||
onChange={(e) => this.setState({oldPassword: e.target.value, errorMessage: undefined})}
|
||||
/>
|
||||
</div>
|
||||
<div className='newPassword'>
|
||||
<input
|
||||
id='login-newpassword'
|
||||
type='password'
|
||||
placeholder={'Enter new password'}
|
||||
value={this.state.newPassword}
|
||||
onChange={(e) => this.setState({newPassword: e.target.value, errorMessage: undefined})}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
filled={true}
|
||||
onClick={() => this.handleSubmit(user.id)}
|
||||
>
|
||||
{'Change password'}
|
||||
</Button>
|
||||
{this.state.errorMessage &&
|
||||
<div className='error'>
|
||||
{this.state.errorMessage}
|
||||
</div>
|
||||
}
|
||||
{this.state.succeeded &&
|
||||
<Link
|
||||
className='succeeded'
|
||||
to='/'
|
||||
>{'Password changed, click to continue.'}</Link>
|
||||
}
|
||||
{!this.state.succeeded &&
|
||||
<Link to='/'>{'Cancel'}</Link>
|
||||
}
|
||||
</>)
|
||||
}
|
||||
return (
|
||||
<Link to='/login'>{'Log in first'}</Link>
|
||||
)
|
||||
}}
|
||||
</UserContext.Consumer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(ChangePasswordPage)
|
|
@ -44,7 +44,7 @@ class RegisterPage extends React.PureComponent<Props, State> {
|
|||
} else if (response.code === 401) {
|
||||
this.setState({errorMessage: 'Invalid registration link, please contact your administrator'})
|
||||
} else {
|
||||
this.setState({errorMessage: response.json.error})
|
||||
this.setState({errorMessage: response.json?.error})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
BASE_URL?=http://www.matternote.com
|
||||
BASE_URL?=http://www.matterdeck.com
|
||||
|
||||
.PHONY: dist
|
||||
dist:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Matternote website
|
||||
# Matterdeck website
|
||||
|
||||
Website for Matternote, built using [Hugo](https://gohugo.io/).
|
||||
Website for Matterdeck, built using [Hugo](https://gohugo.io/).
|
||||
|
||||
## How to build
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ baseURL = "https://tasks.octo.mattermost.com/"
|
|||
canonifyURLs = true
|
||||
#relativeURLs = true
|
||||
|
||||
title = "Matternote"
|
||||
title = "Matterdeck"
|
||||
languageCode = "en-us"
|
||||
publishDir = "../docs"
|
||||
pygmentsCodeFences = true
|
||||
|
@ -48,7 +48,7 @@ pygmentsStyle = "manni"
|
|||
|
||||
# Navigation
|
||||
[params.navigation]
|
||||
brand = "Matternote"
|
||||
brand = "Matterdeck"
|
||||
home = "Home"
|
||||
|
||||
# You can add custom links before or after the default links
|
||||
|
@ -92,7 +92,7 @@ pygmentsStyle = "manni"
|
|||
|
||||
# Hero section
|
||||
[params.hero]
|
||||
title = "Get Matternote"
|
||||
title = "Get Matterdeck"
|
||||
subtitle = ''
|
||||
|
||||
# Intro section
|
||||
|
@ -100,7 +100,7 @@ pygmentsStyle = "manni"
|
|||
[params.intro]
|
||||
[[params.intro.item]]
|
||||
title = "Download"
|
||||
description = "Download Matternote here."
|
||||
description = "Download Matterdeck here."
|
||||
url = "download/personal-edition"
|
||||
button = "Download Now"
|
||||
icon = "/img/download-icon.svg"
|
||||
|
@ -108,7 +108,7 @@ pygmentsStyle = "manni"
|
|||
|
||||
[[params.intro.item]]
|
||||
title = "Read Guide"
|
||||
description = "Read the User's Guide to ge the most out of Matternote."
|
||||
description = "Read the User's Guide to ge the most out of Matterdeck."
|
||||
url = "guide/user"
|
||||
button = "User's Guide"
|
||||
icon = "/img/use-icon.svg"
|
||||
|
@ -116,7 +116,7 @@ pygmentsStyle = "manni"
|
|||
|
||||
[[params.intro.item]]
|
||||
title = "Contribute"
|
||||
description = "Help build the future of productivity and submit code directly to the Matternote open-source project."
|
||||
description = "Help build the future of productivity and submit code directly to the Matterdeck open-source project."
|
||||
url = "contribute/getting-started"
|
||||
button = "Start Contributing"
|
||||
icon = "/img/contribute-icon.svg"
|
||||
|
|
|
@ -5,7 +5,7 @@ section: "contribute"
|
|||
weight: 1
|
||||
---
|
||||
|
||||
Welcome to the Matternote project!
|
||||
Welcome to the Matterdeck project!
|
||||
|
||||
We're very glad you want to check it out and perhaps contribute code our repository in GitHub.
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ subsection: Getting Started
|
|||
|
||||
Thanks for your interest in contributing code!
|
||||
|
||||
<!-- Come join our [Contributors community channel](https://community.mattermost.com/core/channels/tickets) on the community server, where you can discuss questions with community members and the Matternote core team. -->
|
||||
<!-- Come join our [Contributors community channel](https://community.mattermost.com/core/channels/tickets) on the community server, where you can discuss questions with community members and the Matterdeck core team. -->
|
||||
|
||||
<!-- To help with translations, [see the localization process](https://docs.mattermost.com/developer/localization.html). -->
|
||||
|
||||
|
|
|
@ -5,12 +5,12 @@ weight: 4
|
|||
subsection: Getting Started
|
||||
---
|
||||
|
||||
A core committer is a maintainer on the Matternote project who has merge access to the repositories. They are responsible for reviewing pull requests, cultivating the developer community, and guiding the technical vision of Matternote. If you have a question or need some help, these are the people to ask.
|
||||
A core committer is a maintainer on the Matterdeck project who has merge access to the repositories. They are responsible for reviewing pull requests, cultivating the developer community, and guiding the technical vision of Matterdeck. If you have a question or need some help, these are the people to ask.
|
||||
|
||||
Core Committers
|
||||
---------------
|
||||
|
||||
Below is the list of core committers working on Matternote:
|
||||
Below is the list of core committers working on Matterdeck:
|
||||
|
||||
- **<a name="chen.lim">Chen Lim</a>**
|
||||
- @chen-i.lim on [community.mattermost.com](https://community.mattermost.com/core/messages/@chen-i.lim) and [@chenilim](https://github.com/chenilim) on GitHub
|
||||
|
|
|
@ -5,6 +5,6 @@ section: "download"
|
|||
weight: 1
|
||||
---
|
||||
|
||||
If you are new to Matternote, [Personal Desktop](desktop) is the fastest way to try it out.
|
||||
If you are new to Matterdeck, [Personal Desktop](desktop) is the fastest way to try it out.
|
||||
|
||||
You can also set up [Personal Server](ubuntu) on Ubuntu to use it with your team, and import boards from Personal Desktop.
|
||||
|
|
|
@ -5,7 +5,7 @@ subsection: Personal Edition
|
|||
weight: 2
|
||||
---
|
||||
|
||||
Matternote Personal Server allows your team to work together on shared project boards.
|
||||
Matterdeck Personal Server allows your team to work together on shared project boards.
|
||||
|
||||
Follow these steps it up on an Ubuntu server.
|
||||
|
||||
|
@ -15,7 +15,7 @@ Popular hosted options include:
|
|||
* [Digital Ocean](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-18-04)
|
||||
* [Amazon EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EC2_GetStarted.html)
|
||||
|
||||
## Install Matternote
|
||||
## Install Matterdeck
|
||||
|
||||
[Download the Ubuntu archive package here](/download), then unpack it to /opt/octo:
|
||||
|
||||
|
@ -26,7 +26,7 @@ sudo mv octo /opt
|
|||
|
||||
## Install NGINX
|
||||
|
||||
By default, the Matternote server runs on port 8000 (specified in config.json). We recommend running NGINX as a web proxy to forward http and websocket requests from port 80 to it. To install NGINX, run:
|
||||
By default, the Matterdeck server runs on port 8000 (specified in config.json). We recommend running NGINX as a web proxy to forward http and websocket requests from port 80 to it. To install NGINX, run:
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
|
@ -97,9 +97,13 @@ server {
|
|||
}
|
||||
```
|
||||
|
||||
## Set up TLS on NGINX (Highly recommended)
|
||||
|
||||
For a production server, it's important to set up TLS to encrypt web traffic. Without this, your login passwords and data are unprotected. Refer to the [NGINX TLS guide](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/) and [Let's Encrypt guide](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/) on setting this up.
|
||||
|
||||
## Install Postgresql (Recommended)
|
||||
|
||||
Matternote stores data in a SQLite database by default, but we recommend running against Postgres in production (we've tested against Postgres 10.15). To install, run:
|
||||
Matterdeck stores data in a SQLite database by default, but we recommend running against Postgres in production (we've tested against Postgres 10.15). To install, run:
|
||||
|
||||
```
|
||||
sudo apt install postgresql postgresql-contrib
|
||||
|
@ -123,7 +127,7 @@ Exit the postgres user session:
|
|||
exit
|
||||
```
|
||||
|
||||
Edit the Matternote config.json:
|
||||
Edit the Matterdeck config.json:
|
||||
|
||||
```
|
||||
nano /opt/octo/config.json
|
||||
|
@ -134,7 +138,7 @@ Change the dbconfig setting to use the postgres database you created:
|
|||
"dbconfig": "postgres://tasksuser:tasksuser-password@localhost/octo?sslmode=disable&connect_timeout=10",
|
||||
```
|
||||
|
||||
## Configure Matternote to run as a service
|
||||
## Configure Matterdeck to run as a service
|
||||
|
||||
This will keep the server running across reboots. First, create a new service config file:
|
||||
|
||||
|
@ -145,7 +149,7 @@ sudo nano /lib/systemd/system/octo.service
|
|||
Paste in the following:
|
||||
```
|
||||
[Unit]
|
||||
Description=Tasks server
|
||||
Description=Matterdeck server
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
@ -167,7 +171,7 @@ sudo systemctl enable octo.service
|
|||
|
||||
## Test the server
|
||||
|
||||
At this point, the Matternote server should be running.
|
||||
At this point, the Matterdeck server should be running.
|
||||
|
||||
Test that it's running locally with:
|
||||
```
|
||||
|
@ -178,3 +182,7 @@ curl localhost
|
|||
The first command checks that the server is running on port 8000 (default), and the second checks that NGINX is proxying requests successfully. Both commands should return the same snippet of html.
|
||||
|
||||
To access the server remotely, open a browser to its IP address or domain.
|
||||
|
||||
## Set up the server
|
||||
|
||||
Refer to the [server setup guide](/guide/server-setup/) to complete server setup.
|
14
website/site/content/guide/server-setup/_index.md
Normal file
14
website/site/content/guide/server-setup/_index.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: "Server Setup Guide"
|
||||
date: "2021-01-21T12:01:23-04:00"
|
||||
section: "guide"
|
||||
weight: 1
|
||||
---
|
||||
|
||||
## Registering the first user
|
||||
|
||||
After installing the server, open a browser to the domain you used (or `http://localhost:8000` for local installs). You should be redirected to the login screen. Click the link to register a new user instead, and complete the registration.
|
||||
|
||||
The first user registration will always be permitted, but **subsequent registrations will require an invite link which includes a code**. You can invite additional users by clicking on your username in the top left, then selecting "Invite users".
|
||||
|
||||
You are now ready to use Matterdeck. Please refer to the [user's guide](../user/) for details.
|
|
@ -7,7 +7,7 @@ weight: 2
|
|||
|
||||
## Adding new Boards
|
||||
|
||||
1. Click "+ Add Board" at the lower left of the sidebar to add a new board to Matternote.
|
||||
1. Click "+ Add Board" at the lower left of the sidebar to add a new board to Matterdeck.
|
||||
2. Pick "Project Tasks" from the list of templates.
|
||||
3. This shows the first view of the new board, which is a table of all tasks
|
||||
![image](./all%20tasks.png)
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-9 doc-content">
|
||||
<h3>Matternote Developer Blog</h3>
|
||||
<h3>Matterdeck Developer Blog</h3>
|
||||
{{ range (.Paginate .Data.Pages).Pages.ByDate.Reverse }}
|
||||
{{ .Render "summary" }}
|
||||
{{ end }}
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
|
||||
<p>
|
||||
<a href="download/personal-edition">
|
||||
<img src="img/hero.png" style="max-height: 500px;" />
|
||||
<img src="img/hero.png" style="width: 100%; max-width: 1000px" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Matternote is an easy-to-use project management system, perfect for organizing your personal todos or running team projects.
|
||||
Matterdeck is an easy-to-use project management system, perfect for organizing your personal todos or running team projects.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
@ -20,7 +20,7 @@
|
|||
</p>
|
||||
|
||||
<p>
|
||||
Matternote is open source! <a href="contribute/getting-started/">Check out the source code here</a>, and contribute to the future of this project.
|
||||
Matterdeck is open source! <a href="contribute/getting-started/">Check out the source code here</a>, and contribute to the future of this project.
|
||||
</p>
|
||||
|
||||
<div class="homepage-search row">
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
text { font: lighter 28px sans-serif; fill: #FFFFFF;}
|
||||
</style>
|
||||
<g transform="translate(0, 28)">
|
||||
<text>🎯 Matternote</text>
|
||||
<text>🎯 Matterdeck</text>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 338 B |
|
@ -4,6 +4,6 @@
|
|||
text { font: lighter 28px sans-serif; fill: #222C3B;}
|
||||
</style>
|
||||
<g transform="translate(0, 28)">
|
||||
<text>🎯 Matternote</text>
|
||||
<text>🎯 Matterdeck</text>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 338 B |
|
@ -1,6 +1,6 @@
|
|||
# Page settings
|
||||
baseurl = "localhost:1313"
|
||||
title = "Matternote"
|
||||
title = "Matterdeck"
|
||||
languageCode = "en-us"
|
||||
theme = "hugo-elate-theme"
|
||||
|
||||
|
@ -14,7 +14,7 @@ theme = "hugo-elate-theme"
|
|||
email = ""
|
||||
# Navigation
|
||||
[params.navigation]
|
||||
brand = "Matternote"
|
||||
brand = "Matterdeck"
|
||||
home = "Home"
|
||||
|
||||
# You can add custom links before or after the default links
|
||||
|
@ -34,7 +34,7 @@ theme = "hugo-elate-theme"
|
|||
|
||||
# Hero section
|
||||
[params.hero]
|
||||
title = "Get Matternote"
|
||||
title = "Get Matterdeck"
|
||||
subtitle = ''
|
||||
|
||||
# Intro section
|
||||
|
@ -43,7 +43,7 @@ theme = "hugo-elate-theme"
|
|||
|
||||
[[params.intro.item]]
|
||||
title = "Contribute"
|
||||
description = "Join the hunrdeds of contributors and submit code directly to the Matternote open-source project."
|
||||
description = "Join the hunrdeds of contributors and submit code directly to the Matterdeck open-source project."
|
||||
url = "/contribute/"
|
||||
button = "Start Contributing"
|
||||
icon = "icon-bulb"
|
||||
|
@ -59,7 +59,7 @@ theme = "hugo-elate-theme"
|
|||
|
||||
[[params.intro.item]]
|
||||
title = "Extend"
|
||||
description = "Extend Matternote to fit your purposes."
|
||||
description = "Extend Matterdeck to fit your purposes."
|
||||
url = "#"
|
||||
button = "Start Extending"
|
||||
icon = "icon-rocket"
|
||||
|
|
|
@ -4,5 +4,5 @@ go 1.15
|
|||
|
||||
require (
|
||||
github.com/gonutz/w32 v1.0.0
|
||||
github.com/zserge/lorca v0.1.9
|
||||
github.com/zserge/lorca v0.1.10-0.20200301195127-a3e43396a47e
|
||||
)
|
||||
|
|
|
@ -2,8 +2,16 @@ github.com/gonutz/w32 v1.0.0 h1:3t1z6ZfkFvirjFYBx9pHeHBuKoN/VBVk9yHb/m2Ll/k=
|
|||
github.com/gonutz/w32 v1.0.0/go.mod h1:Rc/YP5K9gv0FW4p6X9qL3E7Y56lfMflEol1fLElfMW4=
|
||||
github.com/zserge/lorca v0.1.9 h1:vbDdkqdp2/rmeg8GlyCewY2X8Z+b0s7BqWyIQL/gakc=
|
||||
github.com/zserge/lorca v0.1.9/go.mod h1:bVmnIbIRlOcoV285KIRSe4bUABKi7R7384Ycuum6e4A=
|
||||
github.com/zserge/lorca v0.1.10-0.20200301195127-a3e43396a47e h1:RqKGfaG8v1WBC6JX5vhG7GocwY1lENlMiraQibyGRsY=
|
||||
github.com/zserge/lorca v0.1.10-0.20200301195127-a3e43396a47e/go.mod h1:bVmnIbIRlOcoV285KIRSe4bUABKi7R7384Ycuum6e4A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
61
win/main.go
61
win/main.go
|
@ -5,14 +5,15 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/gonutz/w32"
|
||||
"github.com/zserge/lorca"
|
||||
)
|
||||
|
||||
func runOctoTasks(ctx context.Context) *exec.Cmd {
|
||||
// cmd := exec.CommandContext(ctx, "bin/octoserver.exe", "--monitorpid", strconv.FormatInt(int64(os.Getpid()), 10))
|
||||
cmd := exec.CommandContext(ctx, "bin/octoserver.exe --single-user")
|
||||
// cmd := exec.CommandContext(ctx, "octoserver.exe", "--monitorpid", strconv.FormatInt(int64(os.Getpid()), 10), "--single-user")
|
||||
cmd := exec.CommandContext(ctx, "octoserver.exe", "--single-user")
|
||||
// cmd := exec.CommandContext(ctx, "cmd.exe", "/C", "start", "./bin/octoserver.exe", "--monitorpid", strconv.FormatInt(int64(os.Getpid()), 10))
|
||||
// cmd := exec.CommandContext(ctx, "cmd.exe", "/C", "start", "./bin/octoserver.exe")
|
||||
|
||||
|
@ -33,6 +34,18 @@ func main() {
|
|||
// log.Printf("PID: %s", strconv.FormatInt(int64(os.Getpid()), 10))
|
||||
hideConsole()
|
||||
|
||||
// Try to find Chrome if Lorca can't find it
|
||||
if len(lorca.ChromeExecutable()) == 0 {
|
||||
chromePath := locateChrome()
|
||||
log.Printf("chromePath: %s", chromePath)
|
||||
if len(chromePath) > 0 {
|
||||
os.Setenv("LORCACHROME", chromePath)
|
||||
} else {
|
||||
lorca.PromptDownload()
|
||||
log.Fatal("Chrome not installed")
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cmd := runOctoTasks(ctx)
|
||||
|
||||
|
@ -61,3 +74,47 @@ func hideConsole() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This duplicates the logic in Lorca, but adds Edge as an option for Windows, fallback to standard logic for other OSes
|
||||
func locateChrome() string {
|
||||
var paths []string
|
||||
switch runtime.GOOS {
|
||||
// case "darwin":
|
||||
// paths = []string{
|
||||
// "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
// "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
||||
// "/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
// "/usr/bin/google-chrome-stable",
|
||||
// "/usr/bin/google-chrome",
|
||||
// "/usr/bin/chromium",
|
||||
// "/usr/bin/chromium-browser",
|
||||
// }
|
||||
case "windows":
|
||||
paths = []string{
|
||||
os.Getenv("LocalAppData") + "/Google/Chrome/Application/chrome.exe",
|
||||
os.Getenv("ProgramFiles") + "/Google/Chrome/Application/chrome.exe",
|
||||
os.Getenv("ProgramFiles(x86)") + "/Google/Chrome/Application/chrome.exe",
|
||||
os.Getenv("LocalAppData") + "/Chromium/Application/chrome.exe",
|
||||
os.Getenv("ProgramFiles") + "/Chromium/Application/chrome.exe",
|
||||
os.Getenv("ProgramFiles(x86)") + "/Chromium/Application/chrome.exe",
|
||||
os.Getenv("ProgramFiles(x86)") + "/Microsoft/Edge/Application/msedge.exe",
|
||||
}
|
||||
// default:
|
||||
// paths = []string{
|
||||
// "/usr/bin/google-chrome-stable",
|
||||
// "/usr/bin/google-chrome",
|
||||
// "/usr/bin/chromium",
|
||||
// "/usr/bin/chromium-browser",
|
||||
// "/snap/bin/chromium",
|
||||
// }
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue