diff --git a/api/database/migrations/0001_user.up.sql b/api/database/migrations/0001_user.up.sql index ccb1d127..2eb90950 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 aabac737..00e9292b 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 3094fd22..a2e6fe67 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 e028c795..af7b50a3 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 3a072531..065262fb 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 eae9bb6b..13f49de0 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