Merge branch 'main' into admin-local

This commit is contained in:
Chen-I Lim 2021-01-22 11:28:45 -08:00
commit 6af0780c17
40 changed files with 574 additions and 150 deletions

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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)
}

View file

@ -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
}

View file

@ -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()

View file

@ -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
}

View file

@ -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

View file

@ -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)",

View file

@ -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}

View file

@ -23,6 +23,9 @@
bottom: 25px;
left: 25px;
}
&.bottom-right {
left: 0;
}
}
.hideOnWidescreen {

View file

@ -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> {

View file

@ -3,6 +3,8 @@
flex-direction: column;
padding: 5px;
color: rgb(var(--main-fg));
font-weight: normal;
line-height: normal;
> .row {
display: flex;

View file

@ -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'>

View file

@ -44,8 +44,9 @@
padding: 3px 20px;
margin-bottom: 5px;
>.username {
>.heading {
line-height: 30px;
cursor: default;
}
>.IconButton {

View file

@ -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>
)
}

View file

@ -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'})}

View file

@ -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)

View file

@ -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',

View 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;
}
}

View 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)

View file

@ -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})
}
}

View file

@ -1,5 +1,5 @@
BASE_URL?=http://www.matternote.com
BASE_URL?=http://www.matterdeck.com
.PHONY: dist
dist:

View file

@ -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

View file

@ -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"

View file

@ -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.

View file

@ -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). -->

View file

@ -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

View file

@ -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.

View file

@ -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.

View 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.

View file

@ -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)

View file

@ -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 }}

View file

@ -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">

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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
)

View file

@ -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=

View file

@ -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 ""
}