diff --git a/api/database/migrations/003_site_info.down.sql b/api/database/migrations/003_site_info.down.sql new file mode 100644 index 00000000..ea7aaf50 --- /dev/null +++ b/api/database/migrations/003_site_info.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS site_info; diff --git a/api/database/migrations/003_site_info.up.sql b/api/database/migrations/003_site_info.up.sql new file mode 100644 index 00000000..1f72c4d4 --- /dev/null +++ b/api/database/migrations/003_site_info.up.sql @@ -0,0 +1,3 @@ +CREATE TABLE IF NOT EXISTS site_info ( + initial_setup boolean NOT NULL DEFAULT TRUE +); diff --git a/api/graphql/generated.go b/api/graphql/generated.go index f29a1334..74eede1a 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -65,10 +65,11 @@ type ComplexityRoot struct { } Mutation struct { - AuthorizeUser func(childComplexity int, username string, password string) int - RegisterUser func(childComplexity int, username string, password string, rootPath string) int - ScanAll func(childComplexity int) int - ScanUser func(childComplexity int, userID string) int + AuthorizeUser func(childComplexity int, username string, password string) 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 string) int } Photo struct { @@ -107,6 +108,7 @@ type ComplexityRoot struct { MyPhotos func(childComplexity int) int MyUser func(childComplexity int) int Photo func(childComplexity int, id string) int + SiteInfo func(childComplexity int) int Users func(childComplexity int) int } @@ -117,6 +119,10 @@ type ComplexityRoot struct { Success func(childComplexity int) int } + SiteInfo struct { + InitialSetup func(childComplexity int) int + } + User struct { Admin func(childComplexity int) int ID func(childComplexity int) int @@ -134,6 +140,7 @@ type AlbumResolver interface { type MutationResolver interface { AuthorizeUser(ctx context.Context, username string, password string) (*models.AuthorizeResult, error) RegisterUser(ctx context.Context, username string, password string, rootPath string) (*models.AuthorizeResult, error) + InitialSetupWizard(ctx context.Context, username string, password string, rootPath string) (*models.AuthorizeResult, error) ScanAll(ctx context.Context) (*models.ScannerResult, error) ScanUser(ctx context.Context, userID string) (*models.ScannerResult, error) } @@ -144,6 +151,7 @@ type PhotoResolver interface { Exif(ctx context.Context, obj *models.Photo) (*models.PhotoExif, error) } type QueryResolver interface { + SiteInfo(ctx context.Context) (*models.SiteInfo, error) Users(ctx context.Context) ([]*models.User, error) MyUser(ctx context.Context) (*models.User, error) MyAlbums(ctx context.Context) ([]*models.Album, error) @@ -249,6 +257,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.initialSetupWizard": + if e.complexity.Mutation.InitialSetupWizard == nil { + break + } + + args, err := ec.field_Mutation_initialSetupWizard_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.InitialSetupWizard(childComplexity, args["username"].(string), args["password"].(string), args["rootPath"].(string)), true + case "Mutation.registerUser": if e.complexity.Mutation.RegisterUser == nil { break @@ -472,6 +492,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Photo(childComplexity, args["id"].(string)), true + case "Query.siteInfo": + if e.complexity.Query.SiteInfo == nil { + break + } + + return e.complexity.Query.SiteInfo(childComplexity), true + case "Query.users": if e.complexity.Query.Users == nil { break @@ -507,6 +534,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ScannerResult.Success(childComplexity), true + case "SiteInfo.initialSetup": + if e.complexity.SiteInfo.InitialSetup == nil { + break + } + + return e.complexity.SiteInfo.InitialSetup(childComplexity), true + case "User.admin": if e.complexity.User.Admin == nil { break @@ -602,6 +636,8 @@ var parsedSchema = gqlparser.MustLoadSchema( scalar Time type Query { + siteInfo: SiteInfo + "List of registered users, must be admin to call" users: [User!]! @isAdmin "Information about the currently logged in user" @@ -621,11 +657,19 @@ type Query { type Mutation { authorizeUser(username: String!, password: String!): AuthorizeResult! + "Registers a new user, must be admin to call" registerUser( username: String! password: String! rootPath: String! - ): AuthorizeResult! + ): AuthorizeResult! @isAdmin + + "Registers the initial user, can only be called if initialSetup from SiteInfo is true" + initialSetupWizard( + username: String! + password: String! + rootPath: String! + ): AuthorizeResult "Scan all users for new photos" scanAll: ScannerResult! @@ -646,6 +690,11 @@ type ScannerResult { message: String } +"General public information about the site" +type SiteInfo { + initialSetup: Boolean! +} + type User { id: ID! username: String! @@ -746,6 +795,36 @@ func (ec *executionContext) field_Mutation_authorizeUser_args(ctx context.Contex 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{}{} + 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["password"]; ok { + arg1, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["password"] = arg1 + var arg2 string + if tmp, ok := rawArgs["rootPath"]; ok { + arg2, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["rootPath"] = arg2 + return args, nil +} + func (ec *executionContext) field_Mutation_registerUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1289,8 +1368,28 @@ func (ec *executionContext) _Mutation_registerUser(ctx context.Context, field gr rctx.Args = args 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().RegisterUser(rctx, args["username"].(string), args["password"].(string), args["rootPath"].(string)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().RegisterUser(rctx, args["username"].(string), args["password"].(string), args["rootPath"].(string)) + } + 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.AuthorizeResult); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/viktorstrate/photoview/api/graphql/models.AuthorizeResult`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -1308,6 +1407,47 @@ func (ec *executionContext) _Mutation_registerUser(ctx context.Context, field gr return ec.marshalNAuthorizeResult2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_initialSetupWizard(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_initialSetupWizard_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) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().InitialSetupWizard(rctx, args["username"].(string), args["password"].(string), args["rootPath"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*models.AuthorizeResult) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOAuthorizeResult2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_scanAll(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { @@ -2109,6 +2249,40 @@ func (ec *executionContext) _PhotoURL_height(ctx context.Context, field graphql. return ec.marshalOInt2int(ctx, field.Selections, res) } +func (ec *executionContext) _Query_siteInfo(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: "Query", + Field: field, + Args: nil, + IsMethod: true, + } + 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.Query().SiteInfo(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*models.SiteInfo) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOSiteInfo2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSiteInfo(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_users(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { @@ -2567,6 +2741,43 @@ func (ec *executionContext) _ScannerResult_message(ctx context.Context, field gr return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } +func (ec *executionContext) _SiteInfo_initialSetup(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (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: "SiteInfo", + Field: field, + Args: nil, + IsMethod: false, + } + 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 obj.InitialSetup, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { @@ -4031,6 +4242,8 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "initialSetupWizard": + out.Values[i] = ec._Mutation_initialSetupWizard(ctx, field) case "scanAll": out.Values[i] = ec._Mutation_scanAll(ctx, field) if out.Values[i] == graphql.Null { @@ -4217,6 +4430,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Query") + case "siteInfo": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_siteInfo(ctx, field) + return res + }) case "users": field := field out.Concurrently(i, func() (res graphql.Marshaler) { @@ -4337,6 +4561,33 @@ func (ec *executionContext) _ScannerResult(ctx context.Context, sel ast.Selectio return out } +var siteInfoImplementors = []string{"SiteInfo"} + +func (ec *executionContext) _SiteInfo(ctx context.Context, sel ast.SelectionSet, obj *models.SiteInfo) graphql.Marshaler { + fields := graphql.CollectFields(ec.RequestContext, sel, siteInfoImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("SiteInfo") + case "initialSetup": + out.Values[i] = ec._SiteInfo_initialSetup(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var userImplementors = []string{"User"} func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *models.User) graphql.Marshaler { @@ -5036,6 +5287,17 @@ func (ec *executionContext) marshalOAlbum2ᚖgithubᚗcomᚋviktorstrateᚋphoto return ec._Album(ctx, sel, v) } +func (ec *executionContext) marshalOAuthorizeResult2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx context.Context, sel ast.SelectionSet, v models.AuthorizeResult) graphql.Marshaler { + return ec._AuthorizeResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalOAuthorizeResult2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx context.Context, sel ast.SelectionSet, v *models.AuthorizeResult) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._AuthorizeResult(ctx, sel, v) +} + func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v interface{}) (bool, error) { return graphql.UnmarshalBoolean(v) } @@ -5201,6 +5463,17 @@ func (ec *executionContext) marshalOPhotoURL2ᚖgithubᚗcomᚋviktorstrateᚋph return ec._PhotoURL(ctx, sel, v) } +func (ec *executionContext) marshalOSiteInfo2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSiteInfo(ctx context.Context, sel ast.SelectionSet, v models.SiteInfo) graphql.Marshaler { + return ec._SiteInfo(ctx, sel, &v) +} + +func (ec *executionContext) marshalOSiteInfo2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSiteInfo(ctx context.Context, sel ast.SelectionSet, v *models.SiteInfo) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._SiteInfo(ctx, sel, v) +} + func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) { return graphql.UnmarshalString(v) } diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go index 925ad413..273f340b 100644 --- a/api/graphql/models/generated.go +++ b/api/graphql/models/generated.go @@ -42,3 +42,8 @@ type ScannerResult struct { Progress *float64 `json:"progress"` Message *string `json:"message"` } + +// General public information about the site +type SiteInfo struct { + InitialSetup bool `json:"initialSetup"` +} diff --git a/api/graphql/models/site_info.go b/api/graphql/models/site_info.go new file mode 100644 index 00000000..0af3ec98 --- /dev/null +++ b/api/graphql/models/site_info.go @@ -0,0 +1,31 @@ +package models + +import ( + "database/sql" +) + +func GetSiteInfo(db *sql.DB) (*SiteInfo, error) { + rows, err := db.Query("SELECT * FROM site_info") + if err != nil { + return nil, err + } + + var initialSetup bool + + if !rows.Next() { + // Entry does not exist + _, err := db.Exec("INSERT INTO site_info (initial_setup) VALUES (true)") + if err != nil { + return nil, err + } + initialSetup = true + } else { + if err := rows.Scan(&initialSetup); err != nil { + return nil, err + } + } + + return &SiteInfo{ + InitialSetup: initialSetup, + }, nil +} diff --git a/api/graphql/resolvers/root.go b/api/graphql/resolvers/root.go index 5cab073d..19e58db2 100644 --- a/api/graphql/resolvers/root.go +++ b/api/graphql/resolvers/root.go @@ -1,9 +1,11 @@ package resolvers import ( + "context" "database/sql" api "github.com/viktorstrate/photoview/api/graphql" + "github.com/viktorstrate/photoview/api/graphql/models" ) //go:generate go run github.com/99designs/gqlgen @@ -23,3 +25,7 @@ func (r *Resolver) Query() api.QueryResolver { type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } + +func (r *queryResolver) SiteInfo(ctx context.Context) (*models.SiteInfo, error) { + return models.GetSiteInfo(r.Database) +} diff --git a/api/graphql/resolvers/user.go b/api/graphql/resolvers/user.go index 14dc257f..d4643d23 100644 --- a/api/graphql/resolvers/user.go +++ b/api/graphql/resolvers/user.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "errors" "github.com/viktorstrate/photoview/api/graphql/auth" "github.com/viktorstrate/photoview/api/graphql/models" @@ -81,3 +82,20 @@ func (r *mutationResolver) RegisterUser(ctx context.Context, username string, pa Token: &token.Value, }, nil } + +func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username string, password string, rootPath string) (*models.AuthorizeResult, error) { + siteInfo, err := models.GetSiteInfo(r.Database) + if err != nil { + return nil, err + } + + if _, err := r.Database.Exec("UPDATE site_info SET initial_setup = false"); err != nil { + return nil, err + } + + if !siteInfo.InitialSetup { + return nil, errors.New("not initial setup") + } + + return r.RegisterUser(ctx, username, password, rootPath) +} diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 18140848..7df5f8f2 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -3,6 +3,8 @@ directive @isAdmin on FIELD_DEFINITION scalar Time type Query { + siteInfo: SiteInfo + "List of registered users, must be admin to call" users: [User!]! @isAdmin "Information about the currently logged in user" @@ -22,11 +24,19 @@ type Query { type Mutation { authorizeUser(username: String!, password: String!): AuthorizeResult! + "Registers a new user, must be admin to call" registerUser( username: String! password: String! rootPath: String! - ): AuthorizeResult! + ): AuthorizeResult! @isAdmin + + "Registers the initial user, can only be called if initialSetup from SiteInfo is true" + initialSetupWizard( + username: String! + password: String! + rootPath: String! + ): AuthorizeResult "Scan all users for new photos" scanAll: ScannerResult! @@ -47,6 +57,11 @@ type ScannerResult { message: String } +"General public information about the site" +type SiteInfo { + initialSetup: Boolean! +} + type User { id: ID! username: String!