diff --git a/api/database/migrations/0001_user.up.sql b/api/database/migrations/0001_user.up.sql
index ccb1d12..2eb9095 100644
--- a/api/database/migrations/0001_user.up.sql
+++ b/api/database/migrations/0001_user.up.sql
@@ -1,7 +1,7 @@
 CREATE TABLE IF NOT EXISTS user (
   user_id int NOT NULL AUTO_INCREMENT,
   username varchar(256) NOT NULL UNIQUE,
-  password varchar(256) NOT NULL,
+  password varchar(256),
   root_path varchar(512),
   admin boolean NOT NULL DEFAULT 0,
 
diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index aabac73..00e9292 100644
--- a/api/graphql/generated.go
+++ b/api/graphql/generated.go
@@ -69,13 +69,16 @@ type ComplexityRoot struct {
 
 	Mutation struct {
 		AuthorizeUser      func(childComplexity int, username string, password string) int
+		CreateUser         func(childComplexity int, username string, rootPath string, password *string, admin bool) int
 		DeleteShareToken   func(childComplexity int, token string) int
+		DeleteUser         func(childComplexity int, id int) int
 		InitialSetupWizard func(childComplexity int, username string, password string, rootPath string) int
 		RegisterUser       func(childComplexity int, username string, password string, rootPath string) int
 		ScanAll            func(childComplexity int) int
 		ScanUser           func(childComplexity int, userID int) int
 		ShareAlbum         func(childComplexity int, albumID int, expire *time.Time, password *string) int
 		SharePhoto         func(childComplexity int, photoID int, expire *time.Time, password *string) int
+		UpdateUser         func(childComplexity int, id int, username *string, rootPath *string, admin *bool) int
 	}
 
 	Photo struct {
@@ -172,6 +175,9 @@ type MutationResolver interface {
 	ShareAlbum(ctx context.Context, albumID int, expire *time.Time, password *string) (*models.ShareToken, error)
 	SharePhoto(ctx context.Context, photoID int, expire *time.Time, password *string) (*models.ShareToken, error)
 	DeleteShareToken(ctx context.Context, token string) (*models.ShareToken, error)
+	UpdateUser(ctx context.Context, id int, username *string, rootPath *string, admin *bool) (*models.User, error)
+	CreateUser(ctx context.Context, username string, rootPath string, password *string, admin bool) (*models.User, error)
+	DeleteUser(ctx context.Context, id int) (*models.User, error)
 }
 type PhotoResolver interface {
 	Thumbnail(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error)
@@ -319,6 +325,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Mutation.AuthorizeUser(childComplexity, args["username"].(string), args["password"].(string)), true
 
+	case "Mutation.createUser":
+		if e.complexity.Mutation.CreateUser == nil {
+			break
+		}
+
+		args, err := ec.field_Mutation_createUser_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Mutation.CreateUser(childComplexity, args["username"].(string), args["rootPath"].(string), args["password"].(*string), args["admin"].(bool)), true
+
 	case "Mutation.deleteShareToken":
 		if e.complexity.Mutation.DeleteShareToken == nil {
 			break
@@ -331,6 +349,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Mutation.DeleteShareToken(childComplexity, args["token"].(string)), true
 
+	case "Mutation.deleteUser":
+		if e.complexity.Mutation.DeleteUser == nil {
+			break
+		}
+
+		args, err := ec.field_Mutation_deleteUser_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Mutation.DeleteUser(childComplexity, args["id"].(int)), true
+
 	case "Mutation.initialSetupWizard":
 		if e.complexity.Mutation.InitialSetupWizard == nil {
 			break
@@ -398,6 +428,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Mutation.SharePhoto(childComplexity, args["photoId"].(int), args["expire"].(*time.Time), args["password"].(*string)), true
 
+	case "Mutation.updateUser":
+		if e.complexity.Mutation.UpdateUser == nil {
+			break
+		}
+
+		args, err := ec.field_Mutation_updateUser_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Mutation.UpdateUser(childComplexity, args["id"].(int), args["username"].(*string), args["rootPath"].(*string), args["admin"].(*bool)), true
+
 	case "Photo.album":
 		if e.complexity.Photo.Album == nil {
 			break
@@ -881,7 +923,7 @@ type Mutation {
   ): AuthorizeResult
 
   "Scan all users for new photos"
-  scanAll: ScannerResult!
+  scanAll: ScannerResult! @isAdmin
   "Scan a single user for new photos"
   scanUser(userId: Int!): ScannerResult!
 
@@ -891,6 +933,20 @@ type Mutation {
   sharePhoto(photoId: Int!, expire: Time, password: String): ShareToken
   "Delete a share token by it's token value"
   deleteShareToken(token: String!): ShareToken
+
+  updateUser(
+    id: Int!
+    username: String
+    rootPath: String
+    admin: Boolean
+  ): User @isAdmin
+  createUser(
+    username: String!
+    rootPath: String!
+    password: String
+    admin: Boolean!
+  ): User @isAdmin
+  deleteUser(id: Int!): User @isAdmin
 }
 
 type AuthorizeResult {
@@ -1066,6 +1122,44 @@ func (ec *executionContext) field_Mutation_authorizeUser_args(ctx context.Contex
 	return args, nil
 }
 
+func (ec *executionContext) field_Mutation_createUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 string
+	if tmp, ok := rawArgs["username"]; ok {
+		arg0, err = ec.unmarshalNString2string(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["username"] = arg0
+	var arg1 string
+	if tmp, ok := rawArgs["rootPath"]; ok {
+		arg1, err = ec.unmarshalNString2string(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["rootPath"] = arg1
+	var arg2 *string
+	if tmp, ok := rawArgs["password"]; ok {
+		arg2, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["password"] = arg2
+	var arg3 bool
+	if tmp, ok := rawArgs["admin"]; ok {
+		arg3, err = ec.unmarshalNBoolean2bool(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["admin"] = arg3
+	return args, nil
+}
+
 func (ec *executionContext) field_Mutation_deleteShareToken_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -1080,6 +1174,20 @@ func (ec *executionContext) field_Mutation_deleteShareToken_args(ctx context.Con
 	return args, nil
 }
 
+func (ec *executionContext) field_Mutation_deleteUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 int
+	if tmp, ok := rawArgs["id"]; ok {
+		arg0, err = ec.unmarshalNInt2int(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["id"] = arg0
+	return args, nil
+}
+
 func (ec *executionContext) field_Mutation_initialSetupWizard_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -1214,6 +1322,44 @@ func (ec *executionContext) field_Mutation_sharePhoto_args(ctx context.Context,
 	return args, nil
 }
 
+func (ec *executionContext) field_Mutation_updateUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 int
+	if tmp, ok := rawArgs["id"]; ok {
+		arg0, err = ec.unmarshalNInt2int(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["id"] = arg0
+	var arg1 *string
+	if tmp, ok := rawArgs["username"]; ok {
+		arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["username"] = arg1
+	var arg2 *string
+	if tmp, ok := rawArgs["rootPath"]; ok {
+		arg2, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["rootPath"] = arg2
+	var arg3 *bool
+	if tmp, ok := rawArgs["admin"]; ok {
+		arg3, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["admin"] = arg3
+	return args, nil
+}
+
 func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -1949,8 +2095,28 @@ func (ec *executionContext) _Mutation_scanAll(ctx context.Context, field graphql
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
-		ctx = rctx // use context from middleware stack in children
-		return ec.resolvers.Mutation().ScanAll(rctx)
+		directive0 := func(rctx context.Context) (interface{}, error) {
+			ctx = rctx // use context from middleware stack in children
+			return ec.resolvers.Mutation().ScanAll(rctx)
+		}
+		directive1 := func(ctx context.Context) (interface{}, error) {
+			if ec.directives.IsAdmin == nil {
+				return nil, errors.New("directive isAdmin is not implemented")
+			}
+			return ec.directives.IsAdmin(ctx, nil, directive0)
+		}
+
+		tmp, err := directive1(rctx)
+		if err != nil {
+			return nil, err
+		}
+		if tmp == nil {
+			return nil, nil
+		}
+		if data, ok := tmp.(*models.ScannerResult); ok {
+			return data, nil
+		}
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/viktorstrate/photoview/api/graphql/models.ScannerResult`, tmp)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -2135,6 +2301,189 @@ func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, fiel
 	return ec.marshalOShareToken2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+		ec.Tracer.EndFieldExecution(ctx)
+	}()
+	rctx := &graphql.ResolverContext{
+		Object:   "Mutation",
+		Field:    field,
+		Args:     nil,
+		IsMethod: true,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Mutation_updateUser_args(ctx, rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	rctx.Args = args
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		directive0 := func(rctx context.Context) (interface{}, error) {
+			ctx = rctx // use context from middleware stack in children
+			return ec.resolvers.Mutation().UpdateUser(rctx, args["id"].(int), args["username"].(*string), args["rootPath"].(*string), args["admin"].(*bool))
+		}
+		directive1 := func(ctx context.Context) (interface{}, error) {
+			if ec.directives.IsAdmin == nil {
+				return nil, errors.New("directive isAdmin is not implemented")
+			}
+			return ec.directives.IsAdmin(ctx, nil, directive0)
+		}
+
+		tmp, err := directive1(rctx)
+		if err != nil {
+			return nil, err
+		}
+		if tmp == nil {
+			return nil, nil
+		}
+		if data, ok := tmp.(*models.User); ok {
+			return data, nil
+		}
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/viktorstrate/photoview/api/graphql/models.User`, tmp)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*models.User)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return ec.marshalOUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+		ec.Tracer.EndFieldExecution(ctx)
+	}()
+	rctx := &graphql.ResolverContext{
+		Object:   "Mutation",
+		Field:    field,
+		Args:     nil,
+		IsMethod: true,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Mutation_createUser_args(ctx, rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	rctx.Args = args
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		directive0 := func(rctx context.Context) (interface{}, error) {
+			ctx = rctx // use context from middleware stack in children
+			return ec.resolvers.Mutation().CreateUser(rctx, args["username"].(string), args["rootPath"].(string), args["password"].(*string), args["admin"].(bool))
+		}
+		directive1 := func(ctx context.Context) (interface{}, error) {
+			if ec.directives.IsAdmin == nil {
+				return nil, errors.New("directive isAdmin is not implemented")
+			}
+			return ec.directives.IsAdmin(ctx, nil, directive0)
+		}
+
+		tmp, err := directive1(rctx)
+		if err != nil {
+			return nil, err
+		}
+		if tmp == nil {
+			return nil, nil
+		}
+		if data, ok := tmp.(*models.User); ok {
+			return data, nil
+		}
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/viktorstrate/photoview/api/graphql/models.User`, tmp)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*models.User)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return ec.marshalOUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+		ec.Tracer.EndFieldExecution(ctx)
+	}()
+	rctx := &graphql.ResolverContext{
+		Object:   "Mutation",
+		Field:    field,
+		Args:     nil,
+		IsMethod: true,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Mutation_deleteUser_args(ctx, rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	rctx.Args = args
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		directive0 := func(rctx context.Context) (interface{}, error) {
+			ctx = rctx // use context from middleware stack in children
+			return ec.resolvers.Mutation().DeleteUser(rctx, args["id"].(int))
+		}
+		directive1 := func(ctx context.Context) (interface{}, error) {
+			if ec.directives.IsAdmin == nil {
+				return nil, errors.New("directive isAdmin is not implemented")
+			}
+			return ec.directives.IsAdmin(ctx, nil, directive0)
+		}
+
+		tmp, err := directive1(rctx)
+		if err != nil {
+			return nil, err
+		}
+		if tmp == nil {
+			return nil, nil
+		}
+		if data, ok := tmp.(*models.User); ok {
+			return data, nil
+		}
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/viktorstrate/photoview/api/graphql/models.User`, tmp)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*models.User)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return ec.marshalOUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _Photo_id(ctx context.Context, field graphql.CollectedField, obj *models.Photo) (ret graphql.Marshaler) {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
 	defer func() {
@@ -5401,6 +5750,12 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 			out.Values[i] = ec._Mutation_sharePhoto(ctx, field)
 		case "deleteShareToken":
 			out.Values[i] = ec._Mutation_deleteShareToken(ctx, field)
+		case "updateUser":
+			out.Values[i] = ec._Mutation_updateUser(ctx, field)
+		case "createUser":
+			out.Values[i] = ec._Mutation_createUser(ctx, field)
+		case "deleteUser":
+			out.Values[i] = ec._Mutation_deleteUser(ctx, field)
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
@@ -7030,6 +7385,17 @@ func (ec *executionContext) marshalOTime2ᚖtimeᚐTime(ctx context.Context, sel
 	return ec.marshalOTime2timeᚐTime(ctx, sel, *v)
 }
 
+func (ec *executionContext) marshalOUser2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v models.User) graphql.Marshaler {
+	return ec._User(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalOUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v *models.User) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._User(ctx, sel, v)
+}
+
 func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler {
 	if v == nil {
 		return graphql.Null
diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go
index 3094fd2..a2e6fe6 100644
--- a/api/graphql/models/user.go
+++ b/api/graphql/models/user.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"log"
+	"os"
 	"time"
 
 	"golang.org/x/crypto/bcrypt"
@@ -14,7 +15,7 @@ import (
 type User struct {
 	UserID   int
 	Username string
-	Password string
+	Password *string
 	RootPath string
 	Admin    bool
 }
@@ -66,7 +67,11 @@ func AuthorizeUser(database *sql.DB, username string, password string) (*User, e
 		}
 	}
 
-	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
+	if user.Password == nil {
+		return nil, errors.New("user does not have a password")
+	}
+
+	if err := bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)); err != nil {
 		if err == bcrypt.ErrMismatchedHashAndPassword {
 			return nil, ErrorInvalidUserCredentials
 		} else {
@@ -77,15 +82,37 @@ func AuthorizeUser(database *sql.DB, username string, password string) (*User, e
 	return user, nil
 }
 
-func RegisterUser(database *sql.Tx, username string, password string, rootPath string) (*User, error) {
-	hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
-	if err != nil {
-		return nil, err
-	}
-	hashedPass := string(hashedPassBytes)
+var ErrorInvalidRootPath = errors.New("invalid root path")
 
-	if _, err := database.Exec("INSERT INTO user (username, password, root_path) VALUES (?, ?, ?)", username, hashedPass, rootPath); err != nil {
-		return nil, err
+func ValidRootPath(rootPath string) bool {
+	_, err := os.Stat(rootPath)
+	if err != nil {
+		log.Printf("Warn: invalid root path: '%s'\n%s\n", rootPath, err)
+		return false
+	}
+
+	return true
+}
+
+func RegisterUser(database *sql.Tx, username string, password *string, rootPath string, admin bool) (*User, error) {
+	if !ValidRootPath(rootPath) {
+		return nil, ErrorInvalidRootPath
+	}
+
+	if password != nil {
+		hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12)
+		if err != nil {
+			return nil, err
+		}
+		hashedPass := string(hashedPassBytes)
+
+		if _, err := database.Exec("INSERT INTO user (username, password, root_path, admin) VALUES (?, ?, ?, ?)", username, hashedPass, rootPath, admin); err != nil {
+			return nil, err
+		}
+	} else {
+		if _, err := database.Exec("INSERT INTO user (username, root_path, admin) VALUES (?, ?, ?)", username, rootPath, admin); err != nil {
+			return nil, err
+		}
 	}
 
 	row := database.QueryRow("SELECT * FROM user WHERE username = ?", username)
diff --git a/api/graphql/resolvers/user.go b/api/graphql/resolvers/user.go
index e028c79..af7b50a 100644
--- a/api/graphql/resolvers/user.go
+++ b/api/graphql/resolvers/user.go
@@ -3,6 +3,7 @@ package resolvers
 import (
 	"context"
 	"errors"
+	"log"
 
 	"github.com/viktorstrate/photoview/api/graphql/auth"
 	"github.com/viktorstrate/photoview/api/graphql/models"
@@ -81,7 +82,7 @@ func (r *mutationResolver) RegisterUser(ctx context.Context, username string, pa
 		return nil, err
 	}
 
-	user, err := models.RegisterUser(tx, username, password, rootPath)
+	user, err := models.RegisterUser(tx, username, &password, rootPath, false)
 	if err != nil {
 		tx.Rollback()
 		return &models.AuthorizeResult{
@@ -127,7 +128,7 @@ func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username stri
 		return nil, err
 	}
 
-	user, err := models.RegisterUser(tx, username, password, rootPath)
+	user, err := models.RegisterUser(tx, username, &password, rootPath, true)
 	if err != nil {
 		tx.Rollback()
 		return &models.AuthorizeResult{
@@ -136,11 +137,6 @@ func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username stri
 		}, nil
 	}
 
-	if _, err := tx.Exec("UPDATE user SET admin = true WHERE user_id = ?", user.UserID); err != nil {
-		tx.Rollback()
-		return nil, err
-	}
-
 	token, err := user.GenerateAccessToken(tx)
 	if err != nil {
 		tx.Rollback()
@@ -157,3 +153,108 @@ func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username stri
 		Token:   &token.Value,
 	}, nil
 }
+
+// Admin queries
+func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *string, rootPath *string, admin *bool) (*models.User, error) {
+
+	user_rows, err := r.Database.Query("SELECT * FROM user WHERE user_id = ?", id)
+	if err != nil {
+		return nil, err
+	}
+	if user_rows.Next() == false {
+		return nil, errors.New("user not found")
+	}
+	user_rows.Close()
+
+	update_str := ""
+	update_args := make([]interface{}, 0)
+
+	if username != nil {
+		update_str += "username = ?, "
+		update_args = append(update_args, username)
+	}
+	if rootPath != nil {
+		if !models.ValidRootPath(*rootPath) {
+			return nil, errors.New("invalid root path")
+		}
+
+		update_str += "root_path = ?, "
+		update_args = append(update_args, rootPath)
+	}
+	if admin != nil {
+		update_str += "admin = ?, "
+		update_args = append(update_args, admin)
+	}
+
+	if len(update_str) == 0 {
+		return nil, errors.New("no updates requested")
+	}
+
+	update_str = update_str[:len(update_str)-2]
+	log.Printf("Updating user with update string: %s\n", update_str)
+
+	update_args = append(update_args, id)
+
+	res, err := r.Database.Exec("UPDATE user SET "+update_str+" WHERE user_id = ?", update_args...)
+	if err != nil {
+		return nil, err
+	}
+	rows_aff, err := res.RowsAffected()
+	if err != nil {
+		return nil, err
+	}
+	if rows_aff == 0 {
+		return nil, errors.New("no users were updated")
+	}
+
+	row := r.Database.QueryRow("SELECT * FROM user WHERE user_id = ?", id)
+	user, err := models.NewUserFromRow(row)
+	if err != nil {
+		return nil, err
+	}
+
+	return user, nil
+}
+
+func (r *mutationResolver) CreateUser(ctx context.Context, username string, rootPath string, password *string, admin bool) (*models.User, error) {
+	tx, err := r.Database.Begin()
+	if err != nil {
+		return nil, err
+	}
+
+	user, err := models.RegisterUser(tx, username, password, rootPath, admin)
+	if err != nil {
+		tx.Rollback()
+		return nil, err
+	}
+
+	if err := tx.Commit(); err != nil {
+		return nil, err
+	}
+
+	return user, nil
+}
+
+func (r *mutationResolver) DeleteUser(ctx context.Context, id int) (*models.User, error) {
+
+	row := r.Database.QueryRow("SELECT * FROM user WHERE user_id = ?", id)
+	user, err := models.NewUserFromRow(row)
+	if err != nil {
+		return nil, err
+	}
+
+	res, err := r.Database.Exec("DELETE FROM user WHERE user_id = ?", id)
+	if err != nil {
+		return nil, err
+	}
+
+	rows, err := res.RowsAffected()
+	if err != nil {
+		return nil, err
+	}
+	if rows == 0 {
+		return nil, errors.New("no users deleted")
+	}
+
+	return user, nil
+}
diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql
index 3a07253..065262f 100644
--- a/api/graphql/schema.graphql
+++ b/api/graphql/schema.graphql
@@ -53,7 +53,7 @@ type Mutation {
   ): AuthorizeResult
 
   "Scan all users for new photos"
-  scanAll: ScannerResult!
+  scanAll: ScannerResult! @isAdmin
   "Scan a single user for new photos"
   scanUser(userId: Int!): ScannerResult!
 
@@ -63,6 +63,20 @@ type Mutation {
   sharePhoto(photoId: Int!, expire: Time, password: String): ShareToken
   "Delete a share token by it's token value"
   deleteShareToken(token: String!): ShareToken
+
+  updateUser(
+    id: Int!
+    username: String
+    rootPath: String
+    admin: Boolean
+  ): User @isAdmin
+  createUser(
+    username: String!
+    rootPath: String!
+    password: String
+    admin: Boolean!
+  ): User @isAdmin
+  deleteUser(id: Int!): User @isAdmin
 }
 
 type AuthorizeResult {
diff --git a/ui/src/Pages/SettingsPage/AddUserRow.js b/ui/src/Pages/SettingsPage/AddUserRow.js
index eae9bb6..13f49de 100644
--- a/ui/src/Pages/SettingsPage/AddUserRow.js
+++ b/ui/src/Pages/SettingsPage/AddUserRow.js
@@ -5,7 +5,11 @@ import { Table, Button, Input, Checkbox } from 'semantic-ui-react'
 import gql from 'graphql-tag'
 
 const createUserMutation = gql`
-  mutation createUser($username: String, $rootPath: String, $admin: Boolean) {
+  mutation createUser(
+    $username: String!
+    $rootPath: String!
+    $admin: Boolean!
+  ) {
     createUser(username: $username, rootPath: $rootPath, admin: $admin) {
       id
       username
