From c39fe6ec24714224dcf2815f8511e224a7583a3a Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Tue, 2 Feb 2021 22:55:35 -0500 Subject: [PATCH] Basic Auth Context --- graph/generated/generated.go | 666 +++++++++++++++++----- graph/model/models_auth.go | 12 + graph/model/models_db.go | 36 ++ graph/model/models_gen.go | 46 ++ graph/schema.graphqls | 55 +- graph/schema.resolvers.go | 53 +- internal/api/auth.go | 54 +- internal/api/middlewares.go | 37 +- internal/{models/api.go => api/models.go} | 2 +- internal/api/routes.go | 21 +- internal/auth/auth.go | 28 +- internal/db/db.go | 11 +- internal/db/users.go | 9 +- 13 files changed, 828 insertions(+), 202 deletions(-) create mode 100644 graph/model/models_auth.go create mode 100644 graph/model/models_db.go rename internal/{models/api.go => api/models.go} (97%) diff --git a/graph/generated/generated.go b/graph/generated/generated.go index c7823df..bcf5285 100644 --- a/graph/generated/generated.go +++ b/graph/generated/generated.go @@ -42,8 +42,8 @@ type ResolverRoot interface { } type DirectiveRoot struct { - HasRole func(ctx context.Context, obj interface{}, next graphql.Resolver, role model.Role) (res interface{}, err error) - Meta func(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string) (res interface{}, err error) + HasMinRole func(ctx context.Context, obj interface{}, next graphql.Resolver, role model.Role) (res interface{}, err error) + Meta func(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string) (res interface{}, err error) } type ComplexityRoot struct { @@ -59,6 +59,11 @@ type ComplexityRoot struct { PageInfo func(childComplexity int) int } + AuthResponse struct { + Error func(childComplexity int) int + Result func(childComplexity int) int + } + Device struct { CreatedAt func(childComplexity int) int ID func(childComplexity int) int @@ -113,6 +118,9 @@ type ComplexityRoot struct { Albums func(childComplexity int, filter *model.AlbumFilter, count *int, page *int) int Device func(childComplexity int, id string) int Devices func(childComplexity int, filter *model.DeviceFilter, count *int, page *int) int + Login func(childComplexity int, user string, password string) int + Logout func(childComplexity int) int + Me func(childComplexity int) int MediaItem func(childComplexity int, id string) int MediaItems func(childComplexity int, filter *model.MediaItemFilter, count *int, page *int) int Tag func(childComplexity int, id string) int @@ -160,11 +168,14 @@ type MutationResolver interface { CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) } type QueryResolver interface { + Login(ctx context.Context, user string, password string) (model.AuthResult, error) + Logout(ctx context.Context) (model.AuthResult, error) MediaItem(ctx context.Context, id string) (*model.MediaItem, error) Device(ctx context.Context, id string) (*model.Device, error) Album(ctx context.Context, id string) (*model.Album, error) - Tag(ctx context.Context, id string) (*model.Tag, error) User(ctx context.Context, id string) (*model.User, error) + Tag(ctx context.Context, id string) (*model.Tag, error) + Me(ctx context.Context) (*model.User, error) MediaItems(ctx context.Context, filter *model.MediaItemFilter, count *int, page *int) (*model.MediaItemResponse, error) Devices(ctx context.Context, filter *model.DeviceFilter, count *int, page *int) (*model.DeviceResponse, error) Albums(ctx context.Context, filter *model.AlbumFilter, count *int, page *int) (*model.AlbumResponse, error) @@ -229,6 +240,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AlbumResponse.PageInfo(childComplexity), true + case "AuthResponse.Error": + if e.complexity.AuthResponse.Error == nil { + break + } + + return e.complexity.AuthResponse.Error(childComplexity), true + + case "AuthResponse.Result": + if e.complexity.AuthResponse.Result == nil { + break + } + + return e.complexity.AuthResponse.Result(childComplexity), true + case "Device.createdAt": if e.complexity.Device.CreatedAt == nil { break @@ -519,6 +544,32 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Devices(childComplexity, args["filter"].(*model.DeviceFilter), args["count"].(*int), args["page"].(*int)), true + case "Query.login": + if e.complexity.Query.Login == nil { + break + } + + args, err := ec.field_Query_login_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.Login(childComplexity, args["user"].(string), args["password"].(string)), true + + case "Query.logout": + if e.complexity.Query.Logout == nil { + break + } + + return e.complexity.Query.Logout(childComplexity), true + + case "Query.me": + if e.complexity.Query.Me == nil { + break + } + + return e.complexity.Query.Me(childComplexity), true + case "Query.mediaItem": if e.complexity.Query.MediaItem == nil { break @@ -787,7 +838,7 @@ scalar Time scalar Upload # https://gqlgen.com/reference/directives/ -directive @hasRole(role: Role!) on FIELD_DEFINITION +directive @hasMinRole(role: Role!) on FIELD_DEFINITION directive @meta( gorm: String, @@ -814,6 +865,20 @@ enum AuthType { LDAP } +# ------------------------------------------------------------ +# ---------------------- Authentication ---------------------- +# ------------------------------------------------------------ + +enum AuthResult { + Success + Failure +} + +type AuthResponse { + Result: AuthResult! + Error: String +} + # ------------------------------------------------------------ # ----------------------- Type Filters ----------------------- # ------------------------------------------------------------ @@ -1095,47 +1160,56 @@ type AlbumResponse { # ------------------------------------------------------------ type Query { + + # Authentication + login( + user: String! + password: String! + ): AuthResult! + logout: AuthResult! @hasMinRole(role: User) + # Single Item - mediaItem(id: ID!): MediaItem! @hasRole(role: User) - device(id: ID!): Device! @hasRole(role: User) - album(id: ID!): Album! @hasRole(role: User) - tag(id: ID!): Tag! @hasRole(role: User) - user(id: ID!): User! @hasRole(role: Admin) + mediaItem(id: ID!): MediaItem! @hasMinRole(role: User) + device(id: ID!): Device! @hasMinRole(role: User) + album(id: ID!): Album! @hasMinRole(role: User) + user(id: ID!): User! @hasMinRole(role: Admin) + tag(id: ID!): Tag! @hasMinRole(role: User) + me: User! @hasMinRole(role: User) # All mediaItems( filter: MediaItemFilter count: Int page: Int - ): MediaItemResponse! @hasRole(role: User) + ): MediaItemResponse! @hasMinRole(role: User) devices( filter: DeviceFilter count: Int page: Int - ): DeviceResponse! @hasRole(role: User) + ): DeviceResponse! @hasMinRole(role: User) albums( filter: AlbumFilter count: Int page: Int - ): AlbumResponse! @hasRole(role: User) + ): AlbumResponse! @hasMinRole(role: User) tags( filter: TagFilter count: Int page: Int - ): TagResponse! @hasRole(role: User) + ): TagResponse! @hasMinRole(role: User) users( filter: UserFilter count: Int page: Int - ): UserResponse! @hasRole(role: Admin) + ): UserResponse! @hasMinRole(role: Admin) } type Mutation { - createMediaItem(input: NewMediaItem!): MediaItem! @hasRole(role: User) - createDevice(input: NewDevice!): Device! @hasRole(role: User) - createAlbum(input: NewAlbum!): Album! @hasRole(role: User) - createTag(input: NewTag!): Tag! @hasRole(role: User) - createUser(input: NewUser!): User! @hasRole(role: Admin) + createMediaItem(input: NewMediaItem!): MediaItem! @hasMinRole(role: User) + createDevice(input: NewDevice!): Device! @hasMinRole(role: User) + createAlbum(input: NewAlbum!): Album! @hasMinRole(role: User) + createTag(input: NewTag!): Tag! @hasMinRole(role: User) + createUser(input: NewUser!): User! @hasMinRole(role: Admin) } `, BuiltIn: false}, } @@ -1145,7 +1219,7 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** -func (ec *executionContext) dir_hasRole_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { +func (ec *executionContext) dir_hasMinRole_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} var arg0 model.Role @@ -1361,6 +1435,30 @@ func (ec *executionContext) field_Query_devices_args(ctx context.Context, rawArg return args, nil } +func (ec *executionContext) field_Query_login_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["user"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("user")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["user"] = arg0 + var arg1 string + if tmp, ok := rawArgs["password"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) + arg1, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["password"] = arg1 + return args, nil +} + func (ec *executionContext) field_Query_mediaItem_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1789,6 +1887,73 @@ func (ec *executionContext) _AlbumResponse_pageInfo(ctx context.Context, field g return ec.marshalNPageInfo2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐPageInfo(ctx, field.Selections, res) } +func (ec *executionContext) _AuthResponse_Result(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "AuthResponse", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Result, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(model.AuthResult) + fc.Result = res + return ec.marshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx, field.Selections, res) +} + +func (ec *executionContext) _AuthResponse_Error(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "AuthResponse", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Error, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _Device_id(ctx context.Context, field graphql.CollectedField, obj *model.Device) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2843,10 +3008,10 @@ func (ec *executionContext) _Mutation_createMediaItem(ctx context.Context, field if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -2909,10 +3074,10 @@ func (ec *executionContext) _Mutation_createDevice(ctx context.Context, field gr if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -2975,10 +3140,10 @@ func (ec *executionContext) _Mutation_createAlbum(ctx context.Context, field gra if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3041,10 +3206,10 @@ func (ec *executionContext) _Mutation_createTag(ctx context.Context, field graph if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3107,10 +3272,10 @@ func (ec *executionContext) _Mutation_createUser(ctx context.Context, field grap if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3245,6 +3410,107 @@ func (ec *executionContext) _PageInfo_total(ctx context.Context, field graphql.C return ec.marshalNInt2int(ctx, field.Selections, res) } +func (ec *executionContext) _Query_login(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Query_login_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Login(rctx, args["user"].(string), args["password"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(model.AuthResult) + fc.Result = res + return ec.marshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx, field.Selections, res) +} + +func (ec *executionContext) _Query_logout(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + 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.Query().Logout(rctx) + } + directive1 := func(ctx context.Context) (interface{}, error) { + role, err := ec.unmarshalNRole2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐRole(ctx, "User") + if err != nil { + return nil, err + } + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") + } + return ec.directives.HasMinRole(ctx, nil, directive0, role) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(model.AuthResult); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be reichard.io/imagini/graph/model.AuthResult`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(model.AuthResult) + fc.Result = res + return ec.marshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_mediaItem(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3278,10 +3544,10 @@ func (ec *executionContext) _Query_mediaItem(ctx context.Context, field graphql. if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3344,10 +3610,10 @@ func (ec *executionContext) _Query_device(ctx context.Context, field graphql.Col if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3410,10 +3676,10 @@ func (ec *executionContext) _Query_album(ctx context.Context, field graphql.Coll if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3443,72 +3709,6 @@ func (ec *executionContext) _Query_album(ctx context.Context, field graphql.Coll return ec.marshalNAlbum2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐAlbum(ctx, field.Selections, res) } -func (ec *executionContext) _Query_tag(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Query", - Field: field, - Args: nil, - IsMethod: true, - IsResolver: true, - } - - ctx = graphql.WithFieldContext(ctx, fc) - rawArgs := field.ArgumentMap(ec.Variables) - args, err := ec.field_Query_tag_args(ctx, rawArgs) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - fc.Args = args - 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.Query().Tag(rctx, args["id"].(string)) - } - directive1 := func(ctx context.Context) (interface{}, error) { - role, err := ec.unmarshalNRole2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐRole(ctx, "User") - if err != nil { - return nil, err - } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") - } - return ec.directives.HasRole(ctx, nil, directive0, role) - } - - tmp, err := directive1(rctx) - if err != nil { - return nil, graphql.ErrorOnPath(ctx, err) - } - if tmp == nil { - return nil, nil - } - if data, ok := tmp.(*model.Tag); ok { - return data, nil - } - return nil, fmt.Errorf(`unexpected type %T from directive, should be *reichard.io/imagini/graph/model.Tag`, tmp) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*model.Tag) - fc.Result = res - return ec.marshalNTag2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐTag(ctx, field.Selections, res) -} - func (ec *executionContext) _Query_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3542,10 +3742,135 @@ func (ec *executionContext) _Query_user(ctx context.Context, field graphql.Colle if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*model.User); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *reichard.io/imagini/graph/model.User`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.User) + fc.Result = res + return ec.marshalNUser2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) +} + +func (ec *executionContext) _Query_tag(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Query_tag_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + 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.Query().Tag(rctx, args["id"].(string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + role, err := ec.unmarshalNRole2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐRole(ctx, "User") + if err != nil { + return nil, err + } + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") + } + return ec.directives.HasMinRole(ctx, nil, directive0, role) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*model.Tag); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *reichard.io/imagini/graph/model.Tag`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Tag) + fc.Result = res + return ec.marshalNTag2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐTag(ctx, field.Selections, res) +} + +func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + 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.Query().Me(rctx) + } + directive1 := func(ctx context.Context) (interface{}, error) { + role, err := ec.unmarshalNRole2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐRole(ctx, "User") + if err != nil { + return nil, err + } + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") + } + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3608,10 +3933,10 @@ func (ec *executionContext) _Query_mediaItems(ctx context.Context, field graphql if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3674,10 +3999,10 @@ func (ec *executionContext) _Query_devices(ctx context.Context, field graphql.Co if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3740,10 +4065,10 @@ func (ec *executionContext) _Query_albums(ctx context.Context, field graphql.Col if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3806,10 +4131,10 @@ func (ec *executionContext) _Query_tags(ctx context.Context, field graphql.Colle if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -3872,10 +4197,10 @@ func (ec *executionContext) _Query_users(ctx context.Context, field graphql.Coll if err != nil { return nil, err } - if ec.directives.HasRole == nil { - return nil, errors.New("directive hasRole is not implemented") + if ec.directives.HasMinRole == nil { + return nil, errors.New("directive hasMinRole is not implemented") } - return ec.directives.HasRole(ctx, nil, directive0, role) + return ec.directives.HasMinRole(ctx, nil, directive0, role) } tmp, err := directive1(rctx) @@ -7006,6 +7331,35 @@ func (ec *executionContext) _AlbumResponse(ctx context.Context, sel ast.Selectio return out } +var authResponseImplementors = []string{"AuthResponse"} + +func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.SelectionSet, obj *model.AuthResponse) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, authResponseImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("AuthResponse") + case "Result": + out.Values[i] = ec._AuthResponse_Result(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "Error": + out.Values[i] = ec._AuthResponse_Error(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var deviceImplementors = []string{"Device"} func (ec *executionContext) _Device(ctx context.Context, sel ast.SelectionSet, obj *model.Device) graphql.Marshaler { @@ -7264,6 +7618,34 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Query") + case "login": + 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_login(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) + case "logout": + 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_logout(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) case "mediaItem": field := field out.Concurrently(i, func() (res graphql.Marshaler) { @@ -7306,6 +7688,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) + case "user": + 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_user(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) case "tag": field := field out.Concurrently(i, func() (res graphql.Marshaler) { @@ -7320,7 +7716,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) - case "user": + case "me": field := field out.Concurrently(i, func() (res graphql.Marshaler) { defer func() { @@ -7328,7 +7724,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Query_user(ctx, field) + res = ec._Query_me(ctx, field) if res == graphql.Null { atomic.AddUint32(&invalids, 1) } @@ -7837,6 +8233,16 @@ func (ec *executionContext) marshalNAlbumResponse2ᚖreichardᚗioᚋimaginiᚋg return ec._AlbumResponse(ctx, sel, v) } +func (ec *executionContext) unmarshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx context.Context, v interface{}) (model.AuthResult, error) { + var res model.AuthResult + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx context.Context, sel ast.SelectionSet, v model.AuthResult) graphql.Marshaler { + return v +} + func (ec *executionContext) unmarshalNAuthType2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthType(ctx context.Context, v interface{}) (model.AuthType, error) { var res model.AuthType err := res.UnmarshalGQL(v) diff --git a/graph/model/models_auth.go b/graph/model/models_auth.go new file mode 100644 index 0000000..5c77bd9 --- /dev/null +++ b/graph/model/models_auth.go @@ -0,0 +1,12 @@ +package model + +import ( + "net/http" +) + +type AuthContext struct { + AccessToken string + RefreshToken string + AuthResponse *http.ResponseWriter + AuthRequest *http.Request +} diff --git a/graph/model/models_db.go b/graph/model/models_db.go new file mode 100644 index 0000000..cd46621 --- /dev/null +++ b/graph/model/models_db.go @@ -0,0 +1,36 @@ +package model + +import ( + "gorm.io/gorm" + "github.com/google/uuid" +) + +func (u *User) BeforeCreate(tx *gorm.DB) (err error) { + newID := uuid.New().String() + u.ID = &newID + return +} + +func (a *Album) BeforeCreate(tx *gorm.DB) (err error) { + newID := uuid.New().String() + a.ID = &newID + return +} + +func (m *MediaItem) BeforeCreate(tx *gorm.DB) (err error) { + newID := uuid.New().String() + m.ID = &newID + return +} + +func (t *Tag) BeforeCreate(tx *gorm.DB) (err error) { + newID := uuid.New().String() + t.ID = &newID + return +} + +func (d *Device) BeforeCreate(tx *gorm.DB) (err error) { + newID := uuid.New().String() + d.ID = &newID + return +} diff --git a/graph/model/models_gen.go b/graph/model/models_gen.go index 695b3f4..ed064a7 100644 --- a/graph/model/models_gen.go +++ b/graph/model/models_gen.go @@ -32,6 +32,11 @@ type AlbumResponse struct { PageInfo *PageInfo `json:"pageInfo" ` } +type AuthResponse struct { + Result AuthResult `json:"Result" ` + Error *string `json:"Error" ` +} + type AuthTypeFilter struct { EqualTo *AuthType `json:"equalTo" ` NotEqualTo *AuthType `json:"notEqualTo" ` @@ -261,6 +266,47 @@ type UserResponse struct { PageInfo *PageInfo `json:"pageInfo" ` } +type AuthResult string + +const ( + AuthResultSuccess AuthResult = "Success" + AuthResultFailure AuthResult = "Failure" +) + +var AllAuthResult = []AuthResult{ + AuthResultSuccess, + AuthResultFailure, +} + +func (e AuthResult) IsValid() bool { + switch e { + case AuthResultSuccess, AuthResultFailure: + return true + } + return false +} + +func (e AuthResult) String() string { + return string(e) +} + +func (e *AuthResult) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = AuthResult(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid AuthResult", str) + } + return nil +} + +func (e AuthResult) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + type AuthType string const ( diff --git a/graph/schema.graphqls b/graph/schema.graphqls index a53c070..3d90eb0 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -4,7 +4,7 @@ scalar Time scalar Upload # https://gqlgen.com/reference/directives/ -directive @hasRole(role: Role!) on FIELD_DEFINITION +directive @hasMinRole(role: Role!) on FIELD_DEFINITION directive @meta( gorm: String, @@ -31,6 +31,20 @@ enum AuthType { LDAP } +# ------------------------------------------------------------ +# ---------------------- Authentication ---------------------- +# ------------------------------------------------------------ + +enum AuthResult { + Success + Failure +} + +type AuthResponse { + Result: AuthResult! + Error: String +} + # ------------------------------------------------------------ # ----------------------- Type Filters ----------------------- # ------------------------------------------------------------ @@ -312,45 +326,54 @@ type AlbumResponse { # ------------------------------------------------------------ type Query { + + # Authentication + login( + user: String! + password: String! + ): AuthResult! + logout: AuthResult! @hasMinRole(role: User) + # Single Item - mediaItem(id: ID!): MediaItem! @hasRole(role: User) - device(id: ID!): Device! @hasRole(role: User) - album(id: ID!): Album! @hasRole(role: User) - tag(id: ID!): Tag! @hasRole(role: User) - user(id: ID!): User! @hasRole(role: Admin) + mediaItem(id: ID!): MediaItem! @hasMinRole(role: User) + device(id: ID!): Device! @hasMinRole(role: User) + album(id: ID!): Album! @hasMinRole(role: User) + user(id: ID!): User! @hasMinRole(role: Admin) + tag(id: ID!): Tag! @hasMinRole(role: User) + me: User! @hasMinRole(role: User) # All mediaItems( filter: MediaItemFilter count: Int page: Int - ): MediaItemResponse! @hasRole(role: User) + ): MediaItemResponse! @hasMinRole(role: User) devices( filter: DeviceFilter count: Int page: Int - ): DeviceResponse! @hasRole(role: User) + ): DeviceResponse! @hasMinRole(role: User) albums( filter: AlbumFilter count: Int page: Int - ): AlbumResponse! @hasRole(role: User) + ): AlbumResponse! @hasMinRole(role: User) tags( filter: TagFilter count: Int page: Int - ): TagResponse! @hasRole(role: User) + ): TagResponse! @hasMinRole(role: User) users( filter: UserFilter count: Int page: Int - ): UserResponse! @hasRole(role: Admin) + ): UserResponse! @hasMinRole(role: Admin) } type Mutation { - createMediaItem(input: NewMediaItem!): MediaItem! @hasRole(role: User) - createDevice(input: NewDevice!): Device! @hasRole(role: User) - createAlbum(input: NewAlbum!): Album! @hasRole(role: User) - createTag(input: NewTag!): Tag! @hasRole(role: User) - createUser(input: NewUser!): User! @hasRole(role: Admin) + createMediaItem(input: NewMediaItem!): MediaItem! @hasMinRole(role: User) + createDevice(input: NewDevice!): Device! @hasMinRole(role: User) + createAlbum(input: NewAlbum!): Album! @hasMinRole(role: User) + createTag(input: NewTag!): Tag! @hasMinRole(role: User) + createUser(input: NewUser!): User! @hasMinRole(role: Admin) } diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index d539daf..3f72070 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -4,6 +4,7 @@ package graph // will be copied through when generating and any unknown code will be moved to the end. import ( + "net/http" "context" "fmt" @@ -28,7 +29,38 @@ func (r *mutationResolver) CreateTag(ctx context.Context, input model.NewTag) (* } func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) { - panic(fmt.Errorf("not implemented")) + user := &model.User{ + Email: input.Email, + Username: input.Username, + FirstName: input.FirstName, + LastName: input.LastName, + Role: input.Role, + AuthType: input.AuthType, + Password: input.Password, + } + + err := r.DB.CreateUser(user) + if err != nil { + panic(fmt.Errorf("DB Error")) + } + + return user, nil +} + +func (r *queryResolver) Login(ctx context.Context, user string, password string) (model.AuthResult, error) { + + // Set Cookie From Context + authContext := ctx.Value("auth").(*model.AuthContext) + resp := *authContext.AuthResponse + testCookie := http.Cookie{Name: "TestCookie", Value: "Test123", Path: "/", HttpOnly: true} + http.SetCookie(resp, &testCookie) + + return model.AuthResultSuccess, nil +} + +func (r *queryResolver) Logout(ctx context.Context) (model.AuthResult, error) { + // panic(fmt.Errorf("not implemented")) + return model.AuthResultSuccess, nil } func (r *queryResolver) MediaItem(ctx context.Context, id string) (*model.MediaItem, error) { @@ -43,11 +75,15 @@ func (r *queryResolver) Album(ctx context.Context, id string) (*model.Album, err panic(fmt.Errorf("not implemented")) } +func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) { + panic(fmt.Errorf("not implemented")) +} + func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) { panic(fmt.Errorf("not implemented")) } -func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) { +func (r *queryResolver) Me(ctx context.Context) (*model.User, error) { panic(fmt.Errorf("not implemented")) } @@ -68,7 +104,18 @@ func (r *queryResolver) Tags(ctx context.Context, filter *model.TagFilter, count } func (r *queryResolver) Users(ctx context.Context, filter *model.UserFilter, count *int, page *int) (*model.UserResponse, error) { - panic(fmt.Errorf("not implemented")) + resp, totalCount, err := r.DB.Users() + if err != nil { + panic(fmt.Errorf("DB Error")) + } + return &model.UserResponse{ + Data: resp, + PageInfo: &model.PageInfo{ + Count: int(totalCount), + Page: 0, + Total: int(totalCount), + }, + }, nil } // Mutation returns generated.MutationResolver implementation. diff --git a/internal/api/auth.go b/internal/api/auth.go index db67217..81ef498 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -4,14 +4,16 @@ import ( "fmt" "time" "strings" + "context" "net/http" "encoding/json" + "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/lestrrat-go/jwx/jwt" + "github.com/99designs/gqlgen/graphql" - "reichard.io/imagini/internal/models" - graphql "reichard.io/imagini/graph/model" + "reichard.io/imagini/graph/model" ) func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) { @@ -22,7 +24,7 @@ func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) { } // Decode into Struct - var creds models.APICredentials + var creds APICredentials err := json.NewDecoder(r.Body).Decode(&creds) if err != nil { errorJSON(w, "Invalid parameters.", http.StatusBadRequest) @@ -36,7 +38,7 @@ func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) { } // Do login - resp, user := api.Auth.AuthenticateUser(creds) + resp, user := api.Auth.AuthenticateUser(creds.User, creds.Password) if !resp { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return @@ -82,7 +84,7 @@ func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) { /** * This will find or create the requested device based on ID and User. **/ -func (api *API) upsertRequestedDevice(user graphql.User, r *http.Request) (graphql.Device, error) { +func (api *API) upsertRequestedDevice(user model.User, r *http.Request) (model.Device, error) { requestedDevice := deriveRequestedDevice(r) requestedDevice.Type = deriveDeviceType(r) requestedDevice.User.ID = user.ID @@ -93,7 +95,7 @@ func (api *API) upsertRequestedDevice(user graphql.User, r *http.Request) (graph return createdDevice, err } - foundDevice, err := api.DB.Device(&graphql.Device{ + foundDevice, err := api.DB.Device(&model.Device{ ID: requestedDevice.ID, User: &user, }) @@ -101,28 +103,28 @@ func (api *API) upsertRequestedDevice(user graphql.User, r *http.Request) (graph return foundDevice, err } -func deriveDeviceType(r *http.Request) graphql.DeviceType { +func deriveDeviceType(r *http.Request) model.DeviceType { userAgent := strings.ToLower(r.Header.Get("User-Agent")) if strings.HasPrefix(userAgent, "ios-imagini"){ - return graphql.DeviceTypeIOs + return model.DeviceTypeIOs } else if strings.HasPrefix(userAgent, "android-imagini"){ - return graphql.DeviceTypeAndroid + return model.DeviceTypeAndroid } else if strings.HasPrefix(userAgent, "chrome"){ - return graphql.DeviceTypeChrome + return model.DeviceTypeChrome } else if strings.HasPrefix(userAgent, "firefox"){ - return graphql.DeviceTypeFirefox + return model.DeviceTypeFirefox } else if strings.HasPrefix(userAgent, "msie"){ - return graphql.DeviceTypeInternetExplorer + return model.DeviceTypeInternetExplorer } else if strings.HasPrefix(userAgent, "edge"){ - return graphql.DeviceTypeEdge + return model.DeviceTypeEdge } else if strings.HasPrefix(userAgent, "safari"){ - return graphql.DeviceTypeSafari + return model.DeviceTypeSafari } - return graphql.DeviceTypeUnknown + return model.DeviceTypeUnknown } -func deriveRequestedDevice(r *http.Request) graphql.Device { - deviceSkeleton := graphql.Device{} +func deriveRequestedDevice(r *http.Request) model.Device { + deviceSkeleton := model.Device{} authHeader := r.Header.Get("X-Imagini-Authorization") splitAuthInfo := strings.Split(authHeader, ",") @@ -202,8 +204,8 @@ func (api *API) refreshAccessToken(w http.ResponseWriter, r *http.Request) (jwt. stringDeviceUUID := deviceUUID.String() // Device & User Skeleton - user := graphql.User{ID: &stringUserUUID} - device := graphql.Device{ID: &stringDeviceUUID} + user := model.User{ID: &stringUserUUID} + device := model.Device{ID: &stringDeviceUUID} // Update token accessTokenString, err := api.Auth.CreateJWTAccessToken(user, device) @@ -230,3 +232,17 @@ func trimQuotes(s string) string { } return s } + +func hasMinRoleDirective(ctx context.Context, obj interface{}, next graphql.Resolver, role model.Role) (res interface{}, err error) { + // if !getCurrentUser(ctx).HasRole(role) { + // // block calling the next resolver + // return nil, fmt.Errorf("Access denied") + // } + + // or let it pass through + return next(ctx) +} + +func metaDirective(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string) (res interface{}, err error){ + return next(ctx) +} diff --git a/internal/api/middlewares.go b/internal/api/middlewares.go index d168fd7..6edb91b 100644 --- a/internal/api/middlewares.go +++ b/internal/api/middlewares.go @@ -1,10 +1,12 @@ package api import ( - "os" - "context" - "net/http" log "github.com/sirupsen/logrus" + "net/http" + "context" + "os" + + "reichard.io/imagini/graph/model" ) type Middleware func(http.Handler) http.HandlerFunc @@ -20,6 +22,35 @@ func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc { return wrapped } +func (api *API) injectContextMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Info("[middleware] Entering testMiddleware...") + authContext := &model.AuthContext{ + AuthResponse: &w, + AuthRequest: r, + } + accessCookie, err := r.Cookie("AccessToken") + if err != nil { + log.Warn("[middleware] AccessToken not found") + } else { + authContext.AccessToken = accessCookie.Value + } + refreshCookie, err := r.Cookie("RefreshToken") + if err != nil { + log.Warn("[middleware] RefreshToken not found") + } else { + authContext.RefreshToken = refreshCookie.Value + } + + // Add context + ctx := context.WithValue(r.Context(), "auth", authContext) + r = r.WithContext(ctx) + + log.Info("[middleware] Exiting testMiddleware...") + next.ServeHTTP(w, r) + }) +} + func (api *API) authMiddleware(next http.Handler) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/models/api.go b/internal/api/models.go similarity index 97% rename from internal/models/api.go rename to internal/api/models.go index 47654a0..8a2345b 100644 --- a/internal/models/api.go +++ b/internal/api/models.go @@ -1,4 +1,4 @@ -package models +package api type APICredentials struct { User string `json:"user"` diff --git a/internal/api/routes.go b/internal/api/routes.go index 9c2603e..41bb097 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -1,33 +1,38 @@ package api import ( - "encoding/json" "net/http" + "encoding/json" - "reichard.io/imagini/internal/models" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" + "reichard.io/imagini/graph" "reichard.io/imagini/graph/generated" ) func (api *API) registerRoutes() { - srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) - // TODO: Provide Authentication for srv + // Set up Directives + c := generated.Config{ Resolvers: &graph.Resolver{ DB: api.DB } } + c.Directives.HasMinRole = hasMinRoleDirective + c.Directives.Meta = metaDirective + srv := handler.NewDefaultServer(generated.NewExecutableSchema(c)) + + // Handle GraphQL api.Router.Handle("/playground", playground.Handler("GraphQL playground", "/query")) - api.Router.Handle("/query", srv) + api.Router.Handle("/query", api.injectContextMiddleware(srv)) + // Handle Resource Route api.Router.HandleFunc("/media/", multipleMiddleware( api.mediaHandler, api.authMiddleware, )) - api.Router.HandleFunc("/logout", api.logoutHandler) - api.Router.HandleFunc("/login", api.loginHandler) + } func errorJSON(w http.ResponseWriter, err string, code int) { - errStruct := &models.APIResponse{Error: &models.APIError{Message: err, Code: int64(code)}} + errStruct := &APIResponse{Error: &APIError{Message: err, Code: int64(code)}} responseJSON(w, errStruct, code) } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 213dfc7..a4dfd8c 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -12,11 +12,9 @@ import ( "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwt" + "reichard.io/imagini/graph/model" "reichard.io/imagini/internal/db" "reichard.io/imagini/internal/config" - - graphql "reichard.io/imagini/graph/model" - "reichard.io/imagini/internal/models" "reichard.io/imagini/internal/session" ) @@ -35,21 +33,21 @@ func NewMgr(db *db.DBManager, c *config.Config) *AuthManager { } } -func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, graphql.User) { +func (auth *AuthManager) AuthenticateUser(user, password string) (bool, model.User) { // Search Objects - userByName := &graphql.User{} - userByName.Username = creds.User + userByName := &model.User{} + userByName.Username = user foundUser, err := auth.DB.User(userByName) if errors.Is(err, gorm.ErrRecordNotFound) { - userByEmail := &graphql.User{} - userByEmail.Email = creds.User + userByEmail := &model.User{} + userByEmail.Email = user foundUser, err = auth.DB.User(userByEmail) } // Error Checking if errors.Is(err, gorm.ErrRecordNotFound) { - log.Warn("[auth] User not found: ", creds.User) + log.Warn("[auth] User not found: ", user) return false, foundUser } else if err != nil { log.Error(err) @@ -61,15 +59,15 @@ func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, gr // Determine Type switch foundUser.AuthType { case "Local": - return authenticateLocalUser(foundUser, creds.Password), foundUser + return authenticateLocalUser(foundUser, password), foundUser case "LDAP": - return authenticateLDAPUser(foundUser, creds.Password), foundUser + return authenticateLDAPUser(foundUser, password), foundUser default: return false, foundUser } } -func (auth *AuthManager) getRole(user graphql.User) string { +func (auth *AuthManager) getRole(user model.User) string { // TODO: Lookup role of user return "User" } @@ -88,7 +86,7 @@ func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, return nil, errors.New("did does not parse") } stringDeviceID := deviceID.String() - device, err := auth.DB.Device(&graphql.Device{ID: &stringDeviceID}) + device, err := auth.DB.Device(&model.Device{ID: &stringDeviceID}) if err != nil { return nil, err } @@ -119,7 +117,7 @@ func (auth *AuthManager) ValidateJWTAccessToken(accessJWT string) (jwt.Token, er return verifiedToken, nil } -func (auth *AuthManager) CreateJWTRefreshToken(user graphql.User, device graphql.Device) (string, error) { +func (auth *AuthManager) CreateJWTRefreshToken(user model.User, device model.Device) (string, error) { // Acquire Refresh Key byteKey := []byte(*device.RefreshKey) @@ -154,7 +152,7 @@ func (auth *AuthManager) CreateJWTRefreshToken(user graphql.User, device graphql return string(signed), nil } -func (auth *AuthManager) CreateJWTAccessToken(user graphql.User, device graphql.Device) (string, error) { +func (auth *AuthManager) CreateJWTAccessToken(user model.User, device model.Device) (string, error) { // Create New Token tm := time.Now() t := jwt.New() diff --git a/internal/db/db.go b/internal/db/db.go index 67994dd..1873598 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -1,14 +1,14 @@ package db import ( - "fmt" - "path" "errors" + "path" + "fmt" - "gorm.io/gorm" + log "github.com/sirupsen/logrus" // "gorm.io/gorm/logger" "gorm.io/driver/sqlite" - log "github.com/sirupsen/logrus" + "gorm.io/gorm" "reichard.io/imagini/internal/config" "reichard.io/imagini/graph/model" @@ -59,6 +59,7 @@ func (dbm *DBManager) bootstrapDatabase() { Username: "admin", AuthType: "Local", Password: &password, + Role: model.RoleAdmin, } err := dbm.CreateUser(user) @@ -68,8 +69,6 @@ func (dbm *DBManager) bootstrapDatabase() { } } -// func (dmb *DBManager) {} - func (dbm *DBManager) QueryBuilder(dest interface{}, params []byte) (int64, error) { // TODO: // - Where Filters diff --git a/internal/db/users.go b/internal/db/users.go index 1d644d6..8853789 100644 --- a/internal/db/users.go +++ b/internal/db/users.go @@ -7,7 +7,7 @@ import ( "reichard.io/imagini/graph/model" ) -func (dbm *DBManager) CreateUser(user *model.User) error { +func (dbm *DBManager) CreateUser (user *model.User) error { log.Info("[db] Creating user: ", user.Username) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*user.Password), bcrypt.DefaultCost) if err != nil { @@ -27,6 +27,13 @@ func (dbm *DBManager) User (user *model.User) (model.User, error) { return foundUser, err } +func (dbm *DBManager) Users () ([]*model.User, int64, error) { + var foundUsers []*model.User + var count int64 + err := dbm.db.Find(&foundUsers).Count(&count).Error + return foundUsers, count, err +} + func (dbm *DBManager) DeleteUser (user model.User) error { return nil }