1
Fork 0

Add initial setup wizard

This commit is contained in:
viktorstrate 2019-07-31 00:55:48 +02:00
parent 7639a10d82
commit 01b90b2fa0
8 changed files with 221 additions and 33 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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