Add initial setup wizard
This commit is contained in:
parent
7639a10d82
commit
01b90b2fa0
|
@ -1,4 +1,6 @@
|
||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import { registerUser } from './users'
|
||||||
|
|
||||||
async function initialSetup(driver) {
|
async function initialSetup(driver) {
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
|
@ -8,8 +10,6 @@ async function initialSetup(driver) {
|
||||||
{
|
{
|
||||||
initialSettings: {
|
initialSettings: {
|
||||||
initialSetup: true,
|
initialSetup: true,
|
||||||
signupEnabled: false,
|
|
||||||
defaultRoot: '/tmp',
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -43,24 +43,26 @@ const Mutation = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
session.close()
|
const userResult = await registerUser(root, args, ctx, info)
|
||||||
|
|
||||||
await session.run(
|
if (!userResult.success) {
|
||||||
`MERGE (info:SiteInfo) ON CREATE SET info = {initialSettings}`,
|
return userResult
|
||||||
{
|
}
|
||||||
initialSettings: {
|
|
||||||
initialSetup: true,
|
const userId = jwt.decode(userResult.token).id
|
||||||
signupEnabled: false,
|
|
||||||
},
|
await session.run(`MATCH (u:User { id: {id} }) SET u.admin = true`, {
|
||||||
}
|
id: userId,
|
||||||
)
|
})
|
||||||
|
|
||||||
|
await session.run(`MATCH (i:SiteInfo) SET i.initialSetup = false`)
|
||||||
|
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: 'Setup successful',
|
status: 'Initial setup successful',
|
||||||
token: null,
|
token: userResult.token,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import uuid from 'uuid'
|
import uuid from 'uuid'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
|
||||||
const Mutation = {
|
const Mutation = {
|
||||||
async authorizeUser(root, args, ctx, info) {
|
async authorizeUser(root, args, ctx, info) {
|
||||||
|
@ -9,7 +10,7 @@ const Mutation = {
|
||||||
let session = ctx.driver.session()
|
let session = ctx.driver.session()
|
||||||
|
|
||||||
let result = await session.run(
|
let result = await session.run(
|
||||||
'MATCH (usr:User {username: {username}, password: {password} }) RETURN usr.id',
|
'MATCH (usr:User {username: {username}, password: {password} }) RETURN usr.id, usr.admin',
|
||||||
{ username, password }
|
{ username, password }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,8 +25,15 @@ const Mutation = {
|
||||||
const record = result.records[0]
|
const record = result.records[0]
|
||||||
|
|
||||||
const userId = record.get('usr.id')
|
const userId = record.get('usr.id')
|
||||||
|
const userAdmin = record.get('usr.admin')
|
||||||
|
|
||||||
const token = jwt.sign({ id: userId }, process.env.JWT_SECRET)
|
let roles = []
|
||||||
|
|
||||||
|
if (userAdmin) {
|
||||||
|
roles.push('admin')
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign({ id: userId, roles }, process.env.JWT_SECRET)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -34,7 +42,7 @@ const Mutation = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async registerUser(root, args, ctx, info) {
|
async registerUser(root, args, ctx, info) {
|
||||||
let { username, password } = args
|
let { username, password, rootPath } = args
|
||||||
|
|
||||||
let session = ctx.driver.session()
|
let session = ctx.driver.session()
|
||||||
let findResult = await session.run(
|
let findResult = await session.run(
|
||||||
|
@ -50,14 +58,22 @@ const Mutation = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await fs.exists(rootPath))) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: 'Root path does not exist on the server',
|
||||||
|
token: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const registerResult = await session.run(
|
const registerResult = await session.run(
|
||||||
'CREATE (n:User { username: {username}, password: {password}, id: {id} }) return n.id',
|
'CREATE (n:User { username: {username}, password: {password}, id: {id}, admin: false, rootPath: {rootPath} }) return n.id',
|
||||||
{ username, password, id: uuid() }
|
{ username, password, id: uuid(), rootPath }
|
||||||
)
|
)
|
||||||
|
|
||||||
let id = registerResult.records[0].get('n.id')
|
let id = registerResult.records[0].get('n.id')
|
||||||
|
|
||||||
const token = jwt.sign({ id }, process.env.JWT_SECRET)
|
const token = jwt.sign({ id, roles: [] }, process.env.JWT_SECRET)
|
||||||
|
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
@ -69,6 +85,8 @@ const Mutation = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const registerUser = Mutation.registerUser
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation,
|
Mutation,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
enum Role {
|
enum Role {
|
||||||
Admin
|
admin
|
||||||
User
|
user
|
||||||
}
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
|
@ -8,7 +8,7 @@ type User {
|
||||||
username: String!
|
username: String!
|
||||||
albums: [Album] @relation(name: "OWNS", direction: "OUT")
|
albums: [Album] @relation(name: "OWNS", direction: "OUT")
|
||||||
# Local filepath for the user's photos
|
# Local filepath for the user's photos
|
||||||
rootPath: String! @hasRole(roles: [Admin])
|
rootPath: String! @hasRole(roles: [admin])
|
||||||
admin: Boolean
|
admin: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,9 +60,7 @@ type Photo {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SiteInfo {
|
type SiteInfo {
|
||||||
signupEnabled: Boolean!
|
|
||||||
initialSetup: Boolean!
|
initialSetup: Boolean!
|
||||||
defaultRoot: String! @hasRole(roles: [Admin])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorizeResult {
|
type AuthorizeResult {
|
||||||
|
@ -78,6 +76,11 @@ type ScannerResult {
|
||||||
progress: Float
|
progress: Float
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Result {
|
||||||
|
success: Boolean!
|
||||||
|
errorMessage: String
|
||||||
|
}
|
||||||
|
|
||||||
type Subscription {
|
type Subscription {
|
||||||
scannerStatusUpdate: ScannerResult
|
scannerStatusUpdate: ScannerResult
|
||||||
}
|
}
|
||||||
|
@ -85,7 +88,15 @@ type Subscription {
|
||||||
type Mutation {
|
type Mutation {
|
||||||
authorizeUser(username: String!, password: String!): AuthorizeResult!
|
authorizeUser(username: String!, password: String!): AuthorizeResult!
|
||||||
@neo4j_ignore
|
@neo4j_ignore
|
||||||
registerUser(username: String!, password: String!): AuthorizeResult!
|
|
||||||
|
registerUser(
|
||||||
|
username: String!
|
||||||
|
password: String!
|
||||||
|
rootPath: String!
|
||||||
|
): AuthorizeResult! @hasRole(roles: [admin]) @neo4j_ignore
|
||||||
|
|
||||||
|
setAdmin(userId: ID!, admin: Boolean!): Result!
|
||||||
|
@hasRole(roles: [admin])
|
||||||
@neo4j_ignore
|
@neo4j_ignore
|
||||||
|
|
||||||
scanAll: ScannerResult! @isAuthenticated @neo4j_ignore
|
scanAll: ScannerResult! @isAuthenticated @neo4j_ignore
|
||||||
|
@ -93,7 +104,7 @@ type Mutation {
|
||||||
initialSetupWizard(
|
initialSetupWizard(
|
||||||
username: String!
|
username: String!
|
||||||
password: String!
|
password: String!
|
||||||
defaultRoot: String!
|
rootPath: String!
|
||||||
): AuthorizeResult @neo4j_ignore
|
): AuthorizeResult @neo4j_ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ const syncSubscription = gql`
|
||||||
subscription syncSubscription {
|
subscription syncSubscription {
|
||||||
scannerStatusUpdate {
|
scannerStatusUpdate {
|
||||||
finished
|
finished
|
||||||
error
|
success
|
||||||
errorMessage
|
errorMessage
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,10 @@ class Messages extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (!localStorage.getItem('token')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Subscription
|
<Subscription
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
import React, { Component } from 'react'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import { Mutation, Query } from 'react-apollo'
|
||||||
|
import { Redirect } from 'react-router-dom'
|
||||||
|
import { Button, Form, Message, Container, Header } from 'semantic-ui-react'
|
||||||
|
import { login, checkInitialSetupQuery } from './LoginPage'
|
||||||
|
|
||||||
|
const initialSetupMutation = gql`
|
||||||
|
mutation InitialSetup(
|
||||||
|
$username: String!
|
||||||
|
$password: String!
|
||||||
|
$rootPath: String!
|
||||||
|
) {
|
||||||
|
initialSetupWizard(
|
||||||
|
username: $username
|
||||||
|
password: $password
|
||||||
|
rootPath: $rootPath
|
||||||
|
) {
|
||||||
|
success
|
||||||
|
status
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
class InitialSetupPage extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
rootPath: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(event, key) {
|
||||||
|
this.setState({ [key]: event.target.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
signIn(event, authorize) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
authorize({
|
||||||
|
variables: {
|
||||||
|
username: this.state.username,
|
||||||
|
password: this.state.password,
|
||||||
|
rootPath: this.state.rootPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (localStorage.getItem('token')) {
|
||||||
|
return <Redirect to="/" />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Container>
|
||||||
|
<Header as="h1" textAlign="center">
|
||||||
|
Initial Setup
|
||||||
|
</Header>
|
||||||
|
<Query query={checkInitialSetupQuery}>
|
||||||
|
{({ loading, error, data }) => {
|
||||||
|
if (data && data.siteInfo && data.siteInfo.initialSetup) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Redirect to="/" />
|
||||||
|
}}
|
||||||
|
</Query>
|
||||||
|
<Mutation
|
||||||
|
mutation={initialSetupMutation}
|
||||||
|
onCompleted={data => {
|
||||||
|
const { success, token } = data.initialSetupWizard
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
login(token)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(authorize, { loading, error, data }) => {
|
||||||
|
let errorMessage = null
|
||||||
|
if (data) {
|
||||||
|
if (!data.initialSetupWizard.success)
|
||||||
|
errorMessage = data.initialSetupWizard.status
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
style={{ width: 500, margin: 'auto' }}
|
||||||
|
error={!!errorMessage}
|
||||||
|
onSubmit={e => this.signIn(e, authorize)}
|
||||||
|
loading={loading || (data && data.initialSetupWizard.success)}
|
||||||
|
>
|
||||||
|
<Form.Field>
|
||||||
|
<label>Username</label>
|
||||||
|
<input onChange={e => this.handleChange(e, 'username')} />
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field>
|
||||||
|
<label>Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
onChange={e => this.handleChange(e, 'password')}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field>
|
||||||
|
<label>Photo Path</label>
|
||||||
|
<input
|
||||||
|
placeholder="/path/to/photos"
|
||||||
|
type="text"
|
||||||
|
onChange={e => this.handleChange(e, 'rootPath')}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<Message error content={errorMessage} />
|
||||||
|
<Button type="submit">Setup Photoview</Button>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Mutation>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InitialSetupPage
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import { Mutation } from 'react-apollo'
|
import { Mutation, Query } from 'react-apollo'
|
||||||
import { Redirect } from 'react-router-dom'
|
import { Redirect } from 'react-router-dom'
|
||||||
import { Button, Form, Message, Container, Header } from 'semantic-ui-react'
|
import { Button, Form, Message, Container, Header } from 'semantic-ui-react'
|
||||||
|
|
||||||
|
@ -14,6 +14,14 @@ const authorizeMutation = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const checkInitialSetupQuery = gql`
|
||||||
|
query CheckInitialSetup {
|
||||||
|
siteInfo {
|
||||||
|
initialSetup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
function setCookie(cname, cvalue, exdays) {
|
function setCookie(cname, cvalue, exdays) {
|
||||||
var d = new Date()
|
var d = new Date()
|
||||||
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000)
|
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000)
|
||||||
|
@ -21,6 +29,12 @@ function setCookie(cname, cvalue, exdays) {
|
||||||
document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/'
|
document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function login(token) {
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
setCookie('token', token, 360)
|
||||||
|
window.location = '/'
|
||||||
|
}
|
||||||
|
|
||||||
class LoginPage extends Component {
|
class LoginPage extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
@ -57,15 +71,22 @@ class LoginPage extends Component {
|
||||||
<Header as="h1" textAlign="center">
|
<Header as="h1" textAlign="center">
|
||||||
Welcome
|
Welcome
|
||||||
</Header>
|
</Header>
|
||||||
|
<Query query={checkInitialSetupQuery}>
|
||||||
|
{({ loading, error, data }) => {
|
||||||
|
if (data && data.siteInfo && data.siteInfo.initialSetup) {
|
||||||
|
return <Redirect to="/initialSetup" />
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}}
|
||||||
|
</Query>
|
||||||
<Mutation
|
<Mutation
|
||||||
mutation={authorizeMutation}
|
mutation={authorizeMutation}
|
||||||
onCompleted={data => {
|
onCompleted={data => {
|
||||||
const { success, token } = data.authorizeUser
|
const { success, token } = data.authorizeUser
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
localStorage.setItem('token', token)
|
login(token)
|
||||||
setCookie('token', token, 360)
|
|
||||||
window.location = '/'
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -6,12 +6,14 @@ import AlbumsPage from './Pages/AllAlbumsPage/AlbumsPage'
|
||||||
import AlbumPage from './Pages/AlbumPage/AlbumPage'
|
import AlbumPage from './Pages/AlbumPage/AlbumPage'
|
||||||
import AuthorizedRoute from './AuthorizedRoute'
|
import AuthorizedRoute from './AuthorizedRoute'
|
||||||
import PhotosPage from './Pages/PhotosPage/PhotosPage'
|
import PhotosPage from './Pages/PhotosPage/PhotosPage'
|
||||||
|
import InitialSetupPage from './Pages/InitialSetupPage'
|
||||||
|
|
||||||
class Routes extends Component {
|
class Routes extends Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/login" component={LoginPage} />
|
<Route path="/login" component={LoginPage} />
|
||||||
|
<Route path="/initialSetup" component={InitialSetupPage} />
|
||||||
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
|
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
|
||||||
<AuthorizedRoute path="/album/:id" component={AlbumPage} />
|
<AuthorizedRoute path="/album/:id" component={AlbumPage} />
|
||||||
<AuthorizedRoute path="/photos" component={PhotosPage} />
|
<AuthorizedRoute path="/photos" component={PhotosPage} />
|
||||||
|
|
|
@ -43,8 +43,10 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
|
||||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (networkError)
|
if (networkError) {
|
||||||
console.log(`[Network error]: ${JSON.stringify(networkError)}`)
|
console.log(`[Network error]: ${JSON.stringify(networkError)}`)
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const authLink = setContext((_, { headers }) => {
|
const authLink = setContext((_, { headers }) => {
|
||||||
|
|
Loading…
Reference in New Issue