diff --git a/api/src/resolvers/siteInfo.js b/api/src/resolvers/siteInfo.js index bd398d54..81c0f043 100644 --- a/api/src/resolvers/siteInfo.js +++ b/api/src/resolvers/siteInfo.js @@ -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, } }, } diff --git a/api/src/resolvers/users.js b/api/src/resolvers/users.js index 8bd84e3f..bf327de8 100644 --- a/api/src/resolvers/users.js +++ b/api/src/resolvers/users.js @@ -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, } diff --git a/api/src/schema.graphql b/api/src/schema.graphql index ca9e8591..f7f58361 100644 --- a/api/src/schema.graphql +++ b/api/src/schema.graphql @@ -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 } diff --git a/ui/src/Messages.js b/ui/src/Messages.js index bafd3823..4e042806 100644 --- a/ui/src/Messages.js +++ b/ui/src/Messages.js @@ -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 ( + } + + return ( +
+ +
+ Initial Setup +
+ + {({ loading, error, data }) => { + if (data && data.siteInfo && data.siteInfo.initialSetup) { + return null + } + + return + }} + + { + 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 ( +
this.signIn(e, authorize)} + loading={loading || (data && data.initialSetupWizard.success)} + > + + + this.handleChange(e, 'username')} /> + + + + this.handleChange(e, 'password')} + /> + + + + this.handleChange(e, 'rootPath')} + /> + + + + + ) + }} +
+
+
+ ) + } +} + +export default InitialSetupPage diff --git a/ui/src/Pages/LoginPage.js b/ui/src/Pages/LoginPage.js index 3cf68e44..b2fdd14f 100644 --- a/ui/src/Pages/LoginPage.js +++ b/ui/src/Pages/LoginPage.js @@ -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 {
Welcome
+ + {({ loading, error, data }) => { + if (data && data.siteInfo && data.siteInfo.initialSetup) { + return + } + + return null + }} + { const { success, token } = data.authorizeUser if (success) { - localStorage.setItem('token', token) - setCookie('token', token, 360) - window.location = '/' + login(token) } }} > diff --git a/ui/src/Routes.js b/ui/src/Routes.js index 8aa28556..ec96fa94 100644 --- a/ui/src/Routes.js +++ b/ui/src/Routes.js @@ -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 ( + diff --git a/ui/src/apolloClient.js b/ui/src/apolloClient.js index 04a05594..a1bccfee 100644 --- a/ui/src/apolloClient.js +++ b/ui/src/apolloClient.js @@ -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 }) => {