Preliminary auth implementation
This commit is contained in:
parent
99cefc5356
commit
f491241c1a
12 changed files with 209 additions and 30 deletions
17
Makefile
17
Makefile
|
@ -24,6 +24,20 @@ server-linux:
|
|||
server-win:
|
||||
cd server; env GOOS=windows GOARCH=amd64 go build -o ../bin/octoserver.exe ./main
|
||||
|
||||
server-single-user:
|
||||
cd server; go build -o ../bin/octoserver ./main --single-user
|
||||
|
||||
server-mac-single-user:
|
||||
mkdir -p bin/mac
|
||||
cd server; env GOOS=darwin GOARCH=amd64 go build -o ../bin/mac/octoserver ./main --single-user
|
||||
|
||||
server-linux-single-user:
|
||||
mkdir -p bin/linux
|
||||
cd server; env GOOS=linux GOARCH=amd64 go build -o ../bin/linux/octoserver ./main --single-user
|
||||
|
||||
server-win-single-user:
|
||||
cd server; env GOOS=windows GOARCH=amd64 go build -o ../bin/octoserver.exe ./main --single-user
|
||||
|
||||
generate:
|
||||
cd server; go get -modfile=go.tools.mod github.com/golang/mock/mockgen
|
||||
cd server; go get -modfile=go.tools.mod github.com/jteeuwen/go-bindata
|
||||
|
@ -45,6 +59,9 @@ server-doc:
|
|||
watch-server:
|
||||
cd server; modd
|
||||
|
||||
watch-server-single-user:
|
||||
cd server; env OCTOSERVER_ARGS=--single-user modd
|
||||
|
||||
webapp:
|
||||
cd webapp; npm run pack
|
||||
|
||||
|
|
|
@ -60,12 +60,12 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if loginData.Type == "normal" {
|
||||
jwtToken, err := a.app().Login(loginData.Username, loginData.Email, loginData.Password, loginData.MfaToken)
|
||||
token, err := a.app().Login(loginData.Username, loginData.Email, loginData.Password, loginData.MfaToken)
|
||||
if err != nil {
|
||||
errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
json, err := json.Marshal(jwtToken)
|
||||
json, err := json.Marshal(map[string]string{"token": token})
|
||||
if err != nil {
|
||||
log.Printf(`ERROR json.Marshal: %v`, r)
|
||||
errorResponse(w, http.StatusInternalServerError, nil)
|
||||
|
@ -111,6 +111,7 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func (a *API) sessionRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf(`Single User: %v`, a.singleUser)
|
||||
if a.singleUser {
|
||||
now := time.Now().Unix()
|
||||
session := &model.Session{
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/model"
|
||||
"github.com/mattermost/mattermost-octo-tasks/server/services/auth"
|
||||
|
@ -38,6 +40,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)
|
||||
return "", errors.New("invalid username or password")
|
||||
}
|
||||
|
||||
|
|
|
@ -56,11 +56,13 @@ func main() {
|
|||
// Command line args
|
||||
pMonitorPid := flag.Int("monitorpid", -1, "a process ID")
|
||||
pPort := flag.Int("port", config.Port, "the port number")
|
||||
pSingleUser := flag.Bool("single-user", false, "single user mode")
|
||||
flag.Parse()
|
||||
|
||||
singleUser := false
|
||||
if pSingleUser := flag.Bool("single-user", false, "single user mode"); pSingleUser != nil {
|
||||
if pSingleUser != nil {
|
||||
singleUser = *pSingleUser
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
if pMonitorPid != nil && *pMonitorPid > 0 {
|
||||
monitorPid(*pMonitorPid)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
**/*.go !**/*_test.go {
|
||||
prep: go build -o ../bin/octoserver ./main
|
||||
daemon +sigterm: cd .. && ./bin/octoserver
|
||||
daemon +sigterm: cd .. && ./bin/octoserver $OCTOSERVER_ARGS
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@ func HashPassword(password string) string {
|
|||
|
||||
// ComparePassword compares the hash
|
||||
func ComparePassword(hash string, password string) bool {
|
||||
|
||||
if len(password) == 0 || len(hash) == 0 {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ const (
|
|||
HEADER_TOKEN = "token"
|
||||
HEADER_AUTH = "Authorization"
|
||||
HEADER_BEARER = "BEARER"
|
||||
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
|
||||
SESSION_COOKIE_TOKEN = "OCTOTASKSAUTHTOKEN"
|
||||
)
|
||||
|
||||
type TokenLocation int
|
||||
|
|
|
@ -7,13 +7,17 @@ import {
|
|||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import client from './octoClient'
|
||||
|
||||
import {getCurrentLanguage, getMessages, storeLanguage} from './i18n'
|
||||
|
||||
import {FlashMessages} from './components/flashMessages'
|
||||
|
||||
import LoginPage from './pages/loginPage'
|
||||
import RegisterPage from './pages/registerPage'
|
||||
import BoardPage from './pages/boardPage'
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
|
@ -35,10 +39,15 @@ export default function App(): JSX.Element {
|
|||
<Route path='/login'>
|
||||
<LoginPage/>
|
||||
</Route>
|
||||
<Route path='/register'>
|
||||
<RegisterPage/>
|
||||
</Route>
|
||||
<Route path='/'>
|
||||
{!client.token && <Redirect to='/login'/>}
|
||||
<BoardPage setLanguage={setAndStoreLanguage}/>
|
||||
</Route>
|
||||
<Route path='/board'>
|
||||
{!client.token && <Redirect to='/login'/>}
|
||||
<BoardPage setLanguage={setAndStoreLanguage}/>
|
||||
</Route>
|
||||
</Switch>
|
||||
|
|
|
@ -8,15 +8,59 @@ import {Utils} from './utils'
|
|||
//
|
||||
class OctoClient {
|
||||
serverUrl: string
|
||||
token?: string
|
||||
|
||||
constructor(serverUrl?: string) {
|
||||
constructor(serverUrl?: string, token?: string) {
|
||||
this.serverUrl = serverUrl || window.location.origin
|
||||
this.token = token
|
||||
Utils.log(`OctoClient serverUrl: ${this.serverUrl}`)
|
||||
}
|
||||
|
||||
async login(username: string, password: string): Promise<boolean> {
|
||||
const path = '/api/v1/login'
|
||||
const body = JSON.stringify({username, password, type: 'normal'})
|
||||
const response = await fetch(this.serverUrl + path, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body,
|
||||
})
|
||||
if (response.status === 200) {
|
||||
const responseJson = (await response.json() || {}) as {token?: string}
|
||||
this.token = responseJson.token
|
||||
if (responseJson.token !== '') {
|
||||
localStorage.setItem('sessionId', this.token || '')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async register(email: string, username: string, password: string): Promise<boolean> {
|
||||
const path = '/api/v1/register'
|
||||
const body = JSON.stringify({email, username, password})
|
||||
const response = await fetch(this.serverUrl + path, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body,
|
||||
})
|
||||
if (response.status === 200) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
headers() {
|
||||
return {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: this.token ? 'Bearer ' + this.token : '',
|
||||
}
|
||||
}
|
||||
|
||||
async getSubtree(rootId?: string, levels = 2): Promise<IBlock[]> {
|
||||
const path = `/api/v1/blocks/${rootId}/subtree?l=${levels}`
|
||||
const response = await fetch(this.serverUrl + path)
|
||||
const response = await fetch(this.serverUrl + path, {headers: this.headers()})
|
||||
const blocks = (await response.json() || []) as IMutableBlock[]
|
||||
this.fixBlocks(blocks)
|
||||
return blocks
|
||||
|
@ -24,7 +68,7 @@ class OctoClient {
|
|||
|
||||
async exportFullArchive(): Promise<IBlock[]> {
|
||||
const path = '/api/v1/blocks/export'
|
||||
const response = await fetch(this.serverUrl + path)
|
||||
const response = await fetch(this.serverUrl + path, {headers: this.headers()})
|
||||
const blocks = (await response.json() || []) as IMutableBlock[]
|
||||
this.fixBlocks(blocks)
|
||||
return blocks
|
||||
|
@ -38,10 +82,7 @@ class OctoClient {
|
|||
const body = JSON.stringify(blocks)
|
||||
return fetch(this.serverUrl + '/api/v1/blocks/import', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: this.headers(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
@ -62,7 +103,7 @@ class OctoClient {
|
|||
}
|
||||
|
||||
private async getBlocksWithPath(path: string): Promise<IBlock[]> {
|
||||
const response = await fetch(this.serverUrl + path)
|
||||
const response = await fetch(this.serverUrl + path, {headers: this.headers()})
|
||||
const blocks = (await response.json() || []) as IMutableBlock[]
|
||||
this.fixBlocks(blocks)
|
||||
return blocks
|
||||
|
@ -98,10 +139,7 @@ class OctoClient {
|
|||
Utils.log(`deleteBlock: ${blockId}`)
|
||||
return fetch(this.serverUrl + `/api/v1/blocks/${encodeURIComponent(blockId)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: this.headers(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -117,10 +155,7 @@ class OctoClient {
|
|||
const body = JSON.stringify(blocks)
|
||||
return fetch(this.serverUrl + '/api/v1/blocks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: this.headers(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
@ -138,6 +173,7 @@ class OctoClient {
|
|||
// TIPTIP: Leave out Content-Type here, it will be automatically set by the browser
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: this.token ? 'Bearer ' + this.token : '',
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
@ -161,6 +197,6 @@ class OctoClient {
|
|||
}
|
||||
}
|
||||
|
||||
const client = new OctoClient()
|
||||
const client = new OctoClient(undefined, localStorage.getItem('sessionId') || '')
|
||||
|
||||
export default client
|
||||
|
|
|
@ -2,26 +2,34 @@
|
|||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
|
||||
import {Utils} from '../utils'
|
||||
import {
|
||||
withRouter,
|
||||
RouteComponentProps,
|
||||
Link,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import Button from '../widgets/buttons/button'
|
||||
import client from '../octoClient'
|
||||
import './loginPage.scss'
|
||||
|
||||
type Props = {
|
||||
}
|
||||
type Props = RouteComponentProps
|
||||
|
||||
type State = {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export default class LoginPage extends React.PureComponent<Props, State> {
|
||||
class LoginPage extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
username: '',
|
||||
password: '',
|
||||
}
|
||||
|
||||
private handleLogin = (): void => {
|
||||
Utils.log('Logging in')
|
||||
private handleLogin = async (): Promise<void> => {
|
||||
const logged = await client.login(this.state.username, this.state.password)
|
||||
if (logged) {
|
||||
this.props.history.push('/')
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
|
@ -45,7 +53,10 @@ export default class LoginPage extends React.PureComponent<Props, State> {
|
|||
/>
|
||||
</div>
|
||||
<Button onClick={this.handleLogin}>{'Login'}</Button>
|
||||
<Link to='/register'>{'or create an account if you don\'t have one'}</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(LoginPage)
|
||||
|
|
27
webapp/src/pages/registerPage.scss
Normal file
27
webapp/src/pages/registerPage.scss
Normal file
|
@ -0,0 +1,27 @@
|
|||
.RegisterPage {
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 15px;
|
||||
width: 450px;
|
||||
height: 400px;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
.email, .username, .password {
|
||||
margin-bottom: 5px;
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 140px;
|
||||
}
|
||||
input {
|
||||
display: inline-block;
|
||||
width: 250px;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.Button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
74
webapp/src/pages/registerPage.tsx
Normal file
74
webapp/src/pages/registerPage.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
// 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 './registerPage.scss'
|
||||
|
||||
type Props = RouteComponentProps
|
||||
|
||||
type State = {
|
||||
email: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
class RegisterPage extends React.PureComponent<Props, 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 logged = await client.login(this.state.username, this.state.password)
|
||||
if (logged) {
|
||||
this.props.history.push('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<div className='RegisterPage'>
|
||||
<div className='email'>
|
||||
<label htmlFor='login-email'>{'Email'}</label>
|
||||
<input
|
||||
id='login-email'
|
||||
value={this.state.email}
|
||||
onChange={(e) => this.setState({email: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<div className='username'>
|
||||
<label htmlFor='login-username'>{'Username'}</label>
|
||||
<input
|
||||
id='login-username'
|
||||
value={this.state.username}
|
||||
onChange={(e) => this.setState({username: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<div className='password'>
|
||||
<label htmlFor='login-username'>{'Password'}</label>
|
||||
<input
|
||||
id='login-password'
|
||||
type='password'
|
||||
value={this.state.password}
|
||||
onChange={(e) => this.setState({password: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={this.handleRegister}>{'Register'}</Button>
|
||||
<Link to='/login'>{'or login if you already have an account'}</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default withRouter(RegisterPage)
|
Loading…
Reference in a new issue