Add initial setup wizard
This commit is contained in:
parent
7639a10d82
commit
01b90b2fa0
|
@ -1,4 +1,6 @@
|
|||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { registerUser } from './users'
|
||||
|
||||
async function initialSetup(driver) {
|
||||
const session = driver.session()
|
||||
|
@ -8,8 +10,6 @@ async function initialSetup(driver) {
|
|||
{
|
||||
initialSettings: {
|
||||
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(
|
||||
`MERGE (info:SiteInfo) ON CREATE SET info = {initialSettings}`,
|
||||
{
|
||||
initialSettings: {
|
||||
initialSetup: true,
|
||||
signupEnabled: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
if (!userResult.success) {
|
||||
return userResult
|
||||
}
|
||||
|
||||
const userId = jwt.decode(userResult.token).id
|
||||
|
||||
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()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: 'Setup successful',
|
||||
token: null,
|
||||
status: 'Initial setup successful',
|
||||
token: userResult.token,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import jwt from 'jsonwebtoken'
|
||||
import uuid from 'uuid'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
const Mutation = {
|
||||
async authorizeUser(root, args, ctx, info) {
|
||||
|
@ -9,7 +10,7 @@ const Mutation = {
|
|||
let session = ctx.driver.session()
|
||||
|
||||
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 }
|
||||
)
|
||||
|
||||
|
@ -24,8 +25,15 @@ const Mutation = {
|
|||
const record = result.records[0]
|
||||
|
||||
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 {
|
||||
success: true,
|
||||
|
@ -34,7 +42,7 @@ const Mutation = {
|
|||
}
|
||||
},
|
||||
async registerUser(root, args, ctx, info) {
|
||||
let { username, password } = args
|
||||
let { username, password, rootPath } = args
|
||||
|
||||
let session = ctx.driver.session()
|
||||
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(
|
||||
'CREATE (n:User { username: {username}, password: {password}, id: {id} }) return n.id',
|
||||
{ username, password, id: uuid() }
|
||||
'CREATE (n:User { username: {username}, password: {password}, id: {id}, admin: false, rootPath: {rootPath} }) return n.id',
|
||||
{ username, password, id: uuid(), rootPath }
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
|
@ -69,6 +85,8 @@ const Mutation = {
|
|||
},
|
||||
}
|
||||
|
||||
export const registerUser = Mutation.registerUser
|
||||
|
||||
export default {
|
||||
Mutation,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
enum Role {
|
||||
Admin
|
||||
User
|
||||
admin
|
||||
user
|
||||
}
|
||||
|
||||
type User {
|
||||
|
@ -8,7 +8,7 @@ type User {
|
|||
username: String!
|
||||
albums: [Album] @relation(name: "OWNS", direction: "OUT")
|
||||
# Local filepath for the user's photos
|
||||
rootPath: String! @hasRole(roles: [Admin])
|
||||
rootPath: String! @hasRole(roles: [admin])
|
||||
admin: Boolean
|
||||
}
|
||||
|
||||
|
@ -60,9 +60,7 @@ type Photo {
|
|||
}
|
||||
|
||||
type SiteInfo {
|
||||
signupEnabled: Boolean!
|
||||
initialSetup: Boolean!
|
||||
defaultRoot: String! @hasRole(roles: [Admin])
|
||||
}
|
||||
|
||||
type AuthorizeResult {
|
||||
|
@ -78,6 +76,11 @@ type ScannerResult {
|
|||
progress: Float
|
||||
}
|
||||
|
||||
type Result {
|
||||
success: Boolean!
|
||||
errorMessage: String
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
scannerStatusUpdate: ScannerResult
|
||||
}
|
||||
|
@ -85,7 +88,15 @@ type Subscription {
|
|||
type Mutation {
|
||||
authorizeUser(username: String!, password: String!): AuthorizeResult!
|
||||
@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
|
||||
|
||||
scanAll: ScannerResult! @isAuthenticated @neo4j_ignore
|
||||
|
@ -93,7 +104,7 @@ type Mutation {
|
|||
initialSetupWizard(
|
||||
username: String!
|
||||
password: String!
|
||||
defaultRoot: String!
|
||||
rootPath: String!
|
||||
): AuthorizeResult @neo4j_ignore
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const syncSubscription = gql`
|
|||
subscription syncSubscription {
|
||||
scannerStatusUpdate {
|
||||
finished
|
||||
error
|
||||
success
|
||||
errorMessage
|
||||
progress
|
||||
}
|
||||
|
@ -56,6 +56,10 @@ class Messages extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (!localStorage.getItem('token')) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<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 gql from 'graphql-tag'
|
||||
import { Mutation } from 'react-apollo'
|
||||
import { Mutation, Query } from 'react-apollo'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
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) {
|
||||
var d = new Date()
|
||||
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000)
|
||||
|
@ -21,6 +29,12 @@ function setCookie(cname, cvalue, exdays) {
|
|||
document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/'
|
||||
}
|
||||
|
||||
export function login(token) {
|
||||
localStorage.setItem('token', token)
|
||||
setCookie('token', token, 360)
|
||||
window.location = '/'
|
||||
}
|
||||
|
||||
class LoginPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
@ -57,15 +71,22 @@ class LoginPage extends Component {
|
|||
<Header as="h1" textAlign="center">
|
||||
Welcome
|
||||
</Header>
|
||||
<Query query={checkInitialSetupQuery}>
|
||||
{({ loading, error, data }) => {
|
||||
if (data && data.siteInfo && data.siteInfo.initialSetup) {
|
||||
return <Redirect to="/initialSetup" />
|
||||
}
|
||||
|
||||
return null
|
||||
}}
|
||||
</Query>
|
||||
<Mutation
|
||||
mutation={authorizeMutation}
|
||||
onCompleted={data => {
|
||||
const { success, token } = data.authorizeUser
|
||||
|
||||
if (success) {
|
||||
localStorage.setItem('token', token)
|
||||
setCookie('token', token, 360)
|
||||
window.location = '/'
|
||||
login(token)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -6,12 +6,14 @@ import AlbumsPage from './Pages/AllAlbumsPage/AlbumsPage'
|
|||
import AlbumPage from './Pages/AlbumPage/AlbumPage'
|
||||
import AuthorizedRoute from './AuthorizedRoute'
|
||||
import PhotosPage from './Pages/PhotosPage/PhotosPage'
|
||||
import InitialSetupPage from './Pages/InitialSetupPage'
|
||||
|
||||
class Routes extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path="/login" component={LoginPage} />
|
||||
<Route path="/initialSetup" component={InitialSetupPage} />
|
||||
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
|
||||
<AuthorizedRoute path="/album/:id" component={AlbumPage} />
|
||||
<AuthorizedRoute path="/photos" component={PhotosPage} />
|
||||
|
|
|
@ -43,8 +43,10 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
|
|||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
||||
)
|
||||
)
|
||||
if (networkError)
|
||||
if (networkError) {
|
||||
console.log(`[Network error]: ${JSON.stringify(networkError)}`)
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
})
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
|
|
Loading…
Reference in New Issue