Enable GraphQL! #1

Merged
evan merged 8 commits from graphql into master 2021-02-09 00:44:20 +00:00
18 changed files with 977 additions and 795 deletions
Showing only changes of commit 082f923482 - Show all commits

View File

@ -1,85 +1,86 @@
package main package main
import ( import (
"os" "os"
"os/signal" "os/signal"
"github.com/urfave/cli/v2"
log "github.com/sirupsen/logrus"
"reichard.io/imagini/cmd/server" log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"reichard.io/imagini/plugin" "reichard.io/imagini/cmd/server"
"github.com/99designs/gqlgen/api"
"github.com/99designs/gqlgen/api"
"github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/config"
"reichard.io/imagini/plugin"
) )
type UTCFormatter struct { type UTCFormatter struct {
log.Formatter log.Formatter
} }
func (u UTCFormatter) Format(e *log.Entry) ([]byte, error) { func (u UTCFormatter) Format(e *log.Entry) ([]byte, error) {
e.Time = e.Time.UTC() e.Time = e.Time.UTC()
return u.Formatter.Format(e) return u.Formatter.Format(e)
} }
func main() { func main() {
log.SetFormatter(UTCFormatter{&log.TextFormatter{FullTimestamp: true}}) log.SetFormatter(UTCFormatter{&log.TextFormatter{FullTimestamp: true}})
app := &cli.App{ app := &cli.App{
Name: "Imagini", Name: "Imagini",
Usage: "A self hosted photo library.", Usage: "A self hosted photo library.",
Commands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: "serve", Name: "serve",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Start Imagini web server.", Usage: "Start Imagini web server.",
Action: cmdServer, Action: cmdServer,
}, },
{ {
Name: "generate", Name: "generate",
Usage: "generate graphql schema", Usage: "generate graphql schema",
Action: cmdGenerate, Action: cmdGenerate,
}, },
}, },
} }
err := app.Run(os.Args) err := app.Run(os.Args)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func cmdServer(ctx *cli.Context) error { func cmdServer(ctx *cli.Context) error {
log.Info("Starting Imagini Server") log.Info("Starting Imagini Server")
server := server.NewServer() server := server.NewServer()
server.StartServer() server.StartServer()
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt) signal.Notify(c, os.Interrupt)
<-c <-c
server.StopServer() server.StopServer()
os.Exit(0) os.Exit(0)
return nil return nil
} }
func cmdGenerate(ctx *cli.Context) error { func cmdGenerate(ctx *cli.Context) error {
log.Info("Generating Imagini Models") log.Info("Generating Imagini Models")
gqlgenConf, err := config.LoadConfigFromDefaultLocations() gqlgenConf, err := config.LoadConfigFromDefaultLocations()
if err != nil { if err != nil {
log.Panic("Failed to load config", err.Error()) log.Panic("Failed to load config", err.Error())
os.Exit(2) os.Exit(2)
} }
log.Info("Generating Schema...") log.Info("Generating Schema...")
err = api.Generate(gqlgenConf, err = api.Generate(gqlgenConf,
api.AddPlugin(plugin.New()), api.AddPlugin(plugin.New()),
) )
log.Info("Schema Generation Done") log.Info("Schema Generation Done")
if err != nil { if err != nil {
log.Panic(err.Error()) log.Panic(err.Error())
os.Exit(3) os.Exit(3)
} }
os.Exit(0) os.Exit(0)
return nil return nil
} }

View File

@ -60,6 +60,7 @@ type ComplexityRoot struct {
} }
AuthResponse struct { AuthResponse struct {
Device func(childComplexity int) int
Error func(childComplexity int) int Error func(childComplexity int) int
Result func(childComplexity int) int Result func(childComplexity int) int
} }
@ -72,6 +73,7 @@ type ComplexityRoot struct {
Type func(childComplexity int) int Type func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
User func(childComplexity int) int User func(childComplexity int) int
UserID func(childComplexity int) int
} }
DeviceResponse struct { DeviceResponse struct {
@ -92,6 +94,7 @@ type ComplexityRoot struct {
Tags func(childComplexity int) int Tags func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
User func(childComplexity int) int User func(childComplexity int) int
UserID func(childComplexity int) int
} }
MediaItemResponse struct { MediaItemResponse struct {
@ -118,7 +121,7 @@ type ComplexityRoot struct {
Albums func(childComplexity int, filter *model.AlbumFilter, count *int, page *int) int Albums func(childComplexity int, filter *model.AlbumFilter, count *int, page *int) int
Device func(childComplexity int, id string) int Device func(childComplexity int, id string) int
Devices func(childComplexity int, filter *model.DeviceFilter, count *int, page *int) int Devices func(childComplexity int, filter *model.DeviceFilter, count *int, page *int) int
Login func(childComplexity int, user string, password string) int Login func(childComplexity int, user string, password string, deviceID *string) int
Logout func(childComplexity int) int Logout func(childComplexity int) int
Me func(childComplexity int) int Me func(childComplexity int) int
MediaItem func(childComplexity int, id string) int MediaItem func(childComplexity int, id string) int
@ -168,8 +171,8 @@ type MutationResolver interface {
CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error)
} }
type QueryResolver interface { type QueryResolver interface {
Login(ctx context.Context, user string, password string) (model.AuthResult, error) Login(ctx context.Context, user string, password string, deviceID *string) (*model.AuthResponse, error)
Logout(ctx context.Context) (model.AuthResult, error) Logout(ctx context.Context) (*model.AuthResponse, error)
MediaItem(ctx context.Context, id string) (*model.MediaItem, error) MediaItem(ctx context.Context, id string) (*model.MediaItem, error)
Device(ctx context.Context, id string) (*model.Device, error) Device(ctx context.Context, id string) (*model.Device, error)
Album(ctx context.Context, id string) (*model.Album, error) Album(ctx context.Context, id string) (*model.Album, error)
@ -240,6 +243,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.AlbumResponse.PageInfo(childComplexity), true return e.complexity.AlbumResponse.PageInfo(childComplexity), true
case "AuthResponse.Device":
if e.complexity.AuthResponse.Device == nil {
break
}
return e.complexity.AuthResponse.Device(childComplexity), true
case "AuthResponse.Error": case "AuthResponse.Error":
if e.complexity.AuthResponse.Error == nil { if e.complexity.AuthResponse.Error == nil {
break break
@ -303,6 +313,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Device.User(childComplexity), true return e.complexity.Device.User(childComplexity), true
case "Device.userID":
if e.complexity.Device.UserID == nil {
break
}
return e.complexity.Device.UserID(childComplexity), true
case "DeviceResponse.data": case "DeviceResponse.data":
if e.complexity.DeviceResponse.Data == nil { if e.complexity.DeviceResponse.Data == nil {
break break
@ -401,6 +418,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.MediaItem.User(childComplexity), true return e.complexity.MediaItem.User(childComplexity), true
case "MediaItem.userID":
if e.complexity.MediaItem.UserID == nil {
break
}
return e.complexity.MediaItem.UserID(childComplexity), true
case "MediaItemResponse.data": case "MediaItemResponse.data":
if e.complexity.MediaItemResponse.Data == nil { if e.complexity.MediaItemResponse.Data == nil {
break break
@ -554,7 +578,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false return 0, false
} }
return e.complexity.Query.Login(childComplexity, args["user"].(string), args["password"].(string)), true return e.complexity.Query.Login(childComplexity, args["user"].(string), args["password"].(string), args["deviceID"].(*string)), true
case "Query.logout": case "Query.logout":
if e.complexity.Query.Logout == nil { if e.complexity.Query.Logout == nil {
@ -876,6 +900,7 @@ enum AuthResult {
type AuthResponse { type AuthResponse {
Result: AuthResult! Result: AuthResult!
Device: Device
Error: String Error: String
} }
@ -972,17 +997,18 @@ input AuthTypeFilter {
# ------------------------------------------------------------ # ------------------------------------------------------------
type Device { type Device {
id: ID @meta(gorm: "primarykey;not null") id: ID! @meta(gorm: "primaryKey;not null")
createdAt: Time createdAt: Time
updatedAt: Time updatedAt: Time
name: String! @meta(gorm: "not null") name: String! @meta(gorm: "not null")
type: DeviceType! @meta(gorm: "default:Unknown;not null") type: DeviceType! @meta(gorm: "default:Unknown;not null")
user: User @meta(gorm: "ForeignKey:ID;not null") userID: ID! @meta(gorm: "not null")
user: User! @meta(gorm: "foreignKey:ID;references:UserID;not null")
refreshKey: String refreshKey: String
} }
type User { type User {
id: ID @meta(gorm: "primarykey;not null") id: ID! @meta(gorm: "primaryKey;not null")
createdAt: Time createdAt: Time
updatedAt: Time updatedAt: Time
email: String! @meta(gorm: "not null;unique") email: String! @meta(gorm: "not null;unique")
@ -995,7 +1021,7 @@ type User {
} }
type MediaItem { type MediaItem {
id: ID @meta(gorm: "primarykey;not null") id: ID! @meta(gorm: "primaryKey;not null")
createdAt: Time createdAt: Time
updatedAt: Time updatedAt: Time
exifDate: Time exifDate: Time
@ -1006,18 +1032,19 @@ type MediaItem {
origName: String! @meta(gorm: "not null") origName: String! @meta(gorm: "not null")
tags: [Tag] @meta(gorm: "many2many:media_tags") tags: [Tag] @meta(gorm: "many2many:media_tags")
albums: [Album] @meta(gorm: "many2many:media_albums") albums: [Album] @meta(gorm: "many2many:media_albums")
user: User @meta(gorm: "ForeignKey:ID;not null") userID: ID! @meta(gorm: "not null")
user: User! @meta(gorm: "foreignKey:ID;references:UserID;not null")
} }
type Tag { type Tag {
id: ID @meta(gorm: "primarykey;not null") id: ID! @meta(gorm: "primaryKey;not null")
createdAt: Time createdAt: Time
updatedAt: Time updatedAt: Time
name: String! @meta(gorm: "unique;not null") name: String! @meta(gorm: "unique;not null")
} }
type Album { type Album {
id: ID @meta(gorm: "primarykey;not null") id: ID! @meta(gorm: "primaryKey;not null")
createdAt: Time createdAt: Time
updatedAt: Time updatedAt: Time
name: String! @meta(gorm: "unique;not null") name: String! @meta(gorm: "unique;not null")
@ -1165,8 +1192,9 @@ type Query {
login( login(
user: String! user: String!
password: String! password: String!
): AuthResult! deviceID: ID
logout: AuthResult! @hasMinRole(role: User) ): AuthResponse!
logout: AuthResponse! @hasMinRole(role: User)
# Single Item # Single Item
mediaItem(id: ID!): MediaItem! @hasMinRole(role: User) mediaItem(id: ID!): MediaItem! @hasMinRole(role: User)
@ -1456,6 +1484,15 @@ func (ec *executionContext) field_Query_login_args(ctx context.Context, rawArgs
} }
} }
args["password"] = arg1 args["password"] = arg1
var arg2 *string
if tmp, ok := rawArgs["deviceID"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("deviceID"))
arg2, err = ec.unmarshalOID2ᚖstring(ctx, tmp)
if err != nil {
return nil, err
}
}
args["deviceID"] = arg2
return args, nil return args, nil
} }
@ -1663,7 +1700,7 @@ func (ec *executionContext) _Album_id(ctx context.Context, field graphql.Collect
return obj.ID, nil return obj.ID, nil
} }
directive1 := func(ctx context.Context) (interface{}, error) { directive1 := func(ctx context.Context) (interface{}, error) {
gorm, err := ec.unmarshalOString2ᚖstring(ctx, "primarykey;not null") gorm, err := ec.unmarshalOString2ᚖstring(ctx, "primaryKey;not null")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1680,21 +1717,24 @@ func (ec *executionContext) _Album_id(ctx context.Context, field graphql.Collect
if tmp == nil { if tmp == nil {
return nil, nil return nil, nil
} }
if data, ok := tmp.(*string); ok { if data, ok := tmp.(string); ok {
return data, nil return data, nil
} }
return nil, fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp) return nil, fmt.Errorf(`unexpected type %T from directive, should be string`, tmp)
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
return graphql.Null return graphql.Null
} }
if resTmp == nil { if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null return graphql.Null
} }
res := resTmp.(*string) res := resTmp.(string)
fc.Result = res fc.Result = res
return ec.marshalOID2ᚖstring(ctx, field.Selections, res) return ec.marshalNID2string(ctx, field.Selections, res)
} }
func (ec *executionContext) _Album_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.Album) (ret graphql.Marshaler) { func (ec *executionContext) _Album_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.Album) (ret graphql.Marshaler) {
@ -1922,6 +1962,38 @@ func (ec *executionContext) _AuthResponse_Result(ctx context.Context, field grap
return ec.marshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx, field.Selections, res) return ec.marshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx, field.Selections, res)
} }
func (ec *executionContext) _AuthResponse_Device(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.Device, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*model.Device)
fc.Result = res
return ec.marshalODevice2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐDevice(ctx, field.Selections, res)
}
func (ec *executionContext) _AuthResponse_Error(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { func (ec *executionContext) _AuthResponse_Error(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -1976,7 +2048,7 @@ func (ec *executionContext) _Device_id(ctx context.Context, field graphql.Collec
return obj.ID, nil return obj.ID, nil
} }
directive1 := func(ctx context.Context) (interface{}, error) { directive1 := func(ctx context.Context) (interface{}, error) {
gorm, err := ec.unmarshalOString2ᚖstring(ctx, "primarykey;not null") gorm, err := ec.unmarshalOString2ᚖstring(ctx, "primaryKey;not null")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1993,21 +2065,24 @@ func (ec *executionContext) _Device_id(ctx context.Context, field graphql.Collec
if tmp == nil { if tmp == nil {
return nil, nil return nil, nil
} }
if data, ok := tmp.(*string); ok { if data, ok := tmp.(string); ok {
return data, nil return data, nil
} }
return nil, fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp) return nil, fmt.Errorf(`unexpected type %T from directive, should be string`, tmp)
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
return graphql.Null return graphql.Null
} }
if resTmp == nil { if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null return graphql.Null
} }
res := resTmp.(*string) res := resTmp.(string)
fc.Result = res fc.Result = res
return ec.marshalOID2ᚖstring(ctx, field.Selections, res) return ec.marshalNID2string(ctx, field.Selections, res)
} }
func (ec *executionContext) _Device_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.Device) (ret graphql.Marshaler) { func (ec *executionContext) _Device_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.Device) (ret graphql.Marshaler) {
@ -2192,6 +2267,65 @@ func (ec *executionContext) _Device_type(ctx context.Context, field graphql.Coll
return ec.marshalNDeviceType2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐDeviceType(ctx, field.Selections, res) return ec.marshalNDeviceType2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐDeviceType(ctx, field.Selections, res)
} }
func (ec *executionContext) _Device_userID(ctx context.Context, field graphql.CollectedField, obj *model.Device) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Device",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
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 obj.UserID, nil
}
directive1 := func(ctx context.Context) (interface{}, error) {
gorm, err := ec.unmarshalOString2ᚖstring(ctx, "not null")
if err != nil {
return nil, err
}
if ec.directives.Meta == nil {
return nil, errors.New("directive meta is not implemented")
}
return ec.directives.Meta(ctx, obj, directive0, gorm)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(string); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be string`, 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.(string)
fc.Result = res
return ec.marshalNID2string(ctx, field.Selections, res)
}
func (ec *executionContext) _Device_user(ctx context.Context, field graphql.CollectedField, obj *model.Device) (ret graphql.Marshaler) { func (ec *executionContext) _Device_user(ctx context.Context, field graphql.CollectedField, obj *model.Device) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -2214,7 +2348,7 @@ func (ec *executionContext) _Device_user(ctx context.Context, field graphql.Coll
return obj.User, nil return obj.User, nil
} }
directive1 := func(ctx context.Context) (interface{}, error) { directive1 := func(ctx context.Context) (interface{}, error) {
gorm, err := ec.unmarshalOString2ᚖstring(ctx, "ForeignKey:ID;not null") gorm, err := ec.unmarshalOString2ᚖstring(ctx, "foreignKey:ID;references:UserID;not null")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -2241,11 +2375,14 @@ func (ec *executionContext) _Device_user(ctx context.Context, field graphql.Coll
return graphql.Null return graphql.Null
} }
if resTmp == nil { if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null return graphql.Null
} }
res := resTmp.(*model.User) res := resTmp.(*model.User)
fc.Result = res fc.Result = res
return ec.marshalOUser2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) return ec.marshalNUser2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐUser(ctx, field.Selections, res)
} }
func (ec *executionContext) _Device_refreshKey(ctx context.Context, field graphql.CollectedField, obj *model.Device) (ret graphql.Marshaler) { func (ec *executionContext) _Device_refreshKey(ctx context.Context, field graphql.CollectedField, obj *model.Device) (ret graphql.Marshaler) {
@ -2369,7 +2506,7 @@ func (ec *executionContext) _MediaItem_id(ctx context.Context, field graphql.Col
return obj.ID, nil return obj.ID, nil
} }
directive1 := func(ctx context.Context) (interface{}, error) { directive1 := func(ctx context.Context) (interface{}, error) {
gorm, err := ec.unmarshalOString2ᚖstring(ctx, "primarykey;not null") gorm, err := ec.unmarshalOString2ᚖstring(ctx, "primaryKey;not null")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -2386,21 +2523,24 @@ func (ec *executionContext) _MediaItem_id(ctx context.Context, field graphql.Col
if tmp == nil { if tmp == nil {
return nil, nil return nil, nil
} }
if data, ok := tmp.(*string); ok { if data, ok := tmp.(string); ok {
return data, nil return data, nil
} }
return nil, fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp) return nil, fmt.Errorf(`unexpected type %T from directive, should be string`, tmp)
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
return graphql.Null return graphql.Null
} }
if resTmp == nil { if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null return graphql.Null
} }
res := resTmp.(*string) res := resTmp.(string)
fc.Result = res fc.Result = res
return ec.marshalOID2ᚖstring(ctx, field.Selections, res) return ec.marshalNID2string(ctx, field.Selections, res)
} }
func (ec *executionContext) _MediaItem_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.MediaItem) (ret graphql.Marshaler) { func (ec *executionContext) _MediaItem_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.MediaItem) (ret graphql.Marshaler) {
@ -2852,6 +2992,65 @@ func (ec *executionContext) _MediaItem_albums(ctx context.Context, field graphql
return ec.marshalOAlbum2ᚕᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐAlbum(ctx, field.Selections, res) return ec.marshalOAlbum2ᚕᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐAlbum(ctx, field.Selections, res)
} }
func (ec *executionContext) _MediaItem_userID(ctx context.Context, field graphql.CollectedField, obj *model.MediaItem) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "MediaItem",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
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 obj.UserID, nil
}
directive1 := func(ctx context.Context) (interface{}, error) {
gorm, err := ec.unmarshalOString2ᚖstring(ctx, "not null")
if err != nil {
return nil, err
}
if ec.directives.Meta == nil {
return nil, errors.New("directive meta is not implemented")
}
return ec.directives.Meta(ctx, obj, directive0, gorm)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(string); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be string`, 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.(string)
fc.Result = res
return ec.marshalNID2string(ctx, field.Selections, res)
}
func (ec *executionContext) _MediaItem_user(ctx context.Context, field graphql.CollectedField, obj *model.MediaItem) (ret graphql.Marshaler) { func (ec *executionContext) _MediaItem_user(ctx context.Context, field graphql.CollectedField, obj *model.MediaItem) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -2874,7 +3073,7 @@ func (ec *executionContext) _MediaItem_user(ctx context.Context, field graphql.C
return obj.User, nil return obj.User, nil
} }
directive1 := func(ctx context.Context) (interface{}, error) { directive1 := func(ctx context.Context) (interface{}, error) {
gorm, err := ec.unmarshalOString2ᚖstring(ctx, "ForeignKey:ID;not null") gorm, err := ec.unmarshalOString2ᚖstring(ctx, "foreignKey:ID;references:UserID;not null")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -2901,11 +3100,14 @@ func (ec *executionContext) _MediaItem_user(ctx context.Context, field graphql.C
return graphql.Null return graphql.Null
} }
if resTmp == nil { if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null return graphql.Null
} }
res := resTmp.(*model.User) res := resTmp.(*model.User)
fc.Result = res fc.Result = res
return ec.marshalOUser2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) return ec.marshalNUser2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐUser(ctx, field.Selections, res)
} }
func (ec *executionContext) _MediaItemResponse_data(ctx context.Context, field graphql.CollectedField, obj *model.MediaItemResponse) (ret graphql.Marshaler) { func (ec *executionContext) _MediaItemResponse_data(ctx context.Context, field graphql.CollectedField, obj *model.MediaItemResponse) (ret graphql.Marshaler) {
@ -3435,7 +3637,7 @@ func (ec *executionContext) _Query_login(ctx context.Context, field graphql.Coll
fc.Args = args fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().Login(rctx, args["user"].(string), args["password"].(string)) return ec.resolvers.Query().Login(rctx, args["user"].(string), args["password"].(string), args["deviceID"].(*string))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -3447,9 +3649,9 @@ func (ec *executionContext) _Query_login(ctx context.Context, field graphql.Coll
} }
return graphql.Null return graphql.Null
} }
res := resTmp.(model.AuthResult) res := resTmp.(*model.AuthResponse)
fc.Result = res fc.Result = res
return ec.marshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx, field.Selections, res) return ec.marshalNAuthResponse2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResponse(ctx, field.Selections, res)
} }
func (ec *executionContext) _Query_logout(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Query_logout(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
@ -3491,10 +3693,10 @@ func (ec *executionContext) _Query_logout(ctx context.Context, field graphql.Col
if tmp == nil { if tmp == nil {
return nil, nil return nil, nil
} }
if data, ok := tmp.(model.AuthResult); ok { if data, ok := tmp.(*model.AuthResponse); ok {
return data, nil return data, nil
} }
return nil, fmt.Errorf(`unexpected type %T from directive, should be reichard.io/imagini/graph/model.AuthResult`, tmp) return nil, fmt.Errorf(`unexpected type %T from directive, should be *reichard.io/imagini/graph/model.AuthResponse`, tmp)
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -3506,9 +3708,9 @@ func (ec *executionContext) _Query_logout(ctx context.Context, field graphql.Col
} }
return graphql.Null return graphql.Null
} }
res := resTmp.(model.AuthResult) res := resTmp.(*model.AuthResponse)
fc.Result = res fc.Result = res
return ec.marshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx, field.Selections, res) return ec.marshalNAuthResponse2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResponse(ctx, field.Selections, res)
} }
func (ec *executionContext) _Query_mediaItem(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Query_mediaItem(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
@ -4323,7 +4525,7 @@ func (ec *executionContext) _Tag_id(ctx context.Context, field graphql.Collected
return obj.ID, nil return obj.ID, nil
} }
directive1 := func(ctx context.Context) (interface{}, error) { directive1 := func(ctx context.Context) (interface{}, error) {
gorm, err := ec.unmarshalOString2ᚖstring(ctx, "primarykey;not null") gorm, err := ec.unmarshalOString2ᚖstring(ctx, "primaryKey;not null")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -4340,21 +4542,24 @@ func (ec *executionContext) _Tag_id(ctx context.Context, field graphql.Collected
if tmp == nil { if tmp == nil {
return nil, nil return nil, nil
} }
if data, ok := tmp.(*string); ok { if data, ok := tmp.(string); ok {
return data, nil return data, nil
} }
return nil, fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp) return nil, fmt.Errorf(`unexpected type %T from directive, should be string`, tmp)
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
return graphql.Null return graphql.Null
} }
if resTmp == nil { if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null return graphql.Null
} }
res := resTmp.(*string) res := resTmp.(string)
fc.Result = res fc.Result = res
return ec.marshalOID2ᚖstring(ctx, field.Selections, res) return ec.marshalNID2string(ctx, field.Selections, res)
} }
func (ec *executionContext) _Tag_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.Tag) (ret graphql.Marshaler) { func (ec *executionContext) _Tag_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.Tag) (ret graphql.Marshaler) {
@ -4569,7 +4774,7 @@ func (ec *executionContext) _User_id(ctx context.Context, field graphql.Collecte
return obj.ID, nil return obj.ID, nil
} }
directive1 := func(ctx context.Context) (interface{}, error) { directive1 := func(ctx context.Context) (interface{}, error) {
gorm, err := ec.unmarshalOString2ᚖstring(ctx, "primarykey;not null") gorm, err := ec.unmarshalOString2ᚖstring(ctx, "primaryKey;not null")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -4586,21 +4791,24 @@ func (ec *executionContext) _User_id(ctx context.Context, field graphql.Collecte
if tmp == nil { if tmp == nil {
return nil, nil return nil, nil
} }
if data, ok := tmp.(*string); ok { if data, ok := tmp.(string); ok {
return data, nil return data, nil
} }
return nil, fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp) return nil, fmt.Errorf(`unexpected type %T from directive, should be string`, tmp)
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
return graphql.Null return graphql.Null
} }
if resTmp == nil { if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null return graphql.Null
} }
res := resTmp.(*string) res := resTmp.(string)
fc.Result = res fc.Result = res
return ec.marshalOID2ᚖstring(ctx, field.Selections, res) return ec.marshalNID2string(ctx, field.Selections, res)
} }
func (ec *executionContext) _User_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { func (ec *executionContext) _User_createdAt(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) {
@ -7282,6 +7490,9 @@ func (ec *executionContext) _Album(ctx context.Context, sel ast.SelectionSet, ob
out.Values[i] = graphql.MarshalString("Album") out.Values[i] = graphql.MarshalString("Album")
case "id": case "id":
out.Values[i] = ec._Album_id(ctx, field, obj) out.Values[i] = ec._Album_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "createdAt": case "createdAt":
out.Values[i] = ec._Album_createdAt(ctx, field, obj) out.Values[i] = ec._Album_createdAt(ctx, field, obj)
case "updatedAt": case "updatedAt":
@ -7347,6 +7558,8 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "Device":
out.Values[i] = ec._AuthResponse_Device(ctx, field, obj)
case "Error": case "Error":
out.Values[i] = ec._AuthResponse_Error(ctx, field, obj) out.Values[i] = ec._AuthResponse_Error(ctx, field, obj)
default: default:
@ -7373,6 +7586,9 @@ func (ec *executionContext) _Device(ctx context.Context, sel ast.SelectionSet, o
out.Values[i] = graphql.MarshalString("Device") out.Values[i] = graphql.MarshalString("Device")
case "id": case "id":
out.Values[i] = ec._Device_id(ctx, field, obj) out.Values[i] = ec._Device_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "createdAt": case "createdAt":
out.Values[i] = ec._Device_createdAt(ctx, field, obj) out.Values[i] = ec._Device_createdAt(ctx, field, obj)
case "updatedAt": case "updatedAt":
@ -7387,8 +7603,16 @@ func (ec *executionContext) _Device(ctx context.Context, sel ast.SelectionSet, o
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "userID":
out.Values[i] = ec._Device_userID(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "user": case "user":
out.Values[i] = ec._Device_user(ctx, field, obj) out.Values[i] = ec._Device_user(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "refreshKey": case "refreshKey":
out.Values[i] = ec._Device_refreshKey(ctx, field, obj) out.Values[i] = ec._Device_refreshKey(ctx, field, obj)
default: default:
@ -7444,6 +7668,9 @@ func (ec *executionContext) _MediaItem(ctx context.Context, sel ast.SelectionSet
out.Values[i] = graphql.MarshalString("MediaItem") out.Values[i] = graphql.MarshalString("MediaItem")
case "id": case "id":
out.Values[i] = ec._MediaItem_id(ctx, field, obj) out.Values[i] = ec._MediaItem_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "createdAt": case "createdAt":
out.Values[i] = ec._MediaItem_createdAt(ctx, field, obj) out.Values[i] = ec._MediaItem_createdAt(ctx, field, obj)
case "updatedAt": case "updatedAt":
@ -7473,8 +7700,16 @@ func (ec *executionContext) _MediaItem(ctx context.Context, sel ast.SelectionSet
out.Values[i] = ec._MediaItem_tags(ctx, field, obj) out.Values[i] = ec._MediaItem_tags(ctx, field, obj)
case "albums": case "albums":
out.Values[i] = ec._MediaItem_albums(ctx, field, obj) out.Values[i] = ec._MediaItem_albums(ctx, field, obj)
case "userID":
out.Values[i] = ec._MediaItem_userID(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "user": case "user":
out.Values[i] = ec._MediaItem_user(ctx, field, obj) out.Values[i] = ec._MediaItem_user(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -7828,6 +8063,9 @@ func (ec *executionContext) _Tag(ctx context.Context, sel ast.SelectionSet, obj
out.Values[i] = graphql.MarshalString("Tag") out.Values[i] = graphql.MarshalString("Tag")
case "id": case "id":
out.Values[i] = ec._Tag_id(ctx, field, obj) out.Values[i] = ec._Tag_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "createdAt": case "createdAt":
out.Values[i] = ec._Tag_createdAt(ctx, field, obj) out.Values[i] = ec._Tag_createdAt(ctx, field, obj)
case "updatedAt": case "updatedAt":
@ -7890,6 +8128,9 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
out.Values[i] = graphql.MarshalString("User") out.Values[i] = graphql.MarshalString("User")
case "id": case "id":
out.Values[i] = ec._User_id(ctx, field, obj) out.Values[i] = ec._User_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "createdAt": case "createdAt":
out.Values[i] = ec._User_createdAt(ctx, field, obj) out.Values[i] = ec._User_createdAt(ctx, field, obj)
case "updatedAt": case "updatedAt":
@ -8233,6 +8474,20 @@ func (ec *executionContext) marshalNAlbumResponse2ᚖreichardᚗioᚋimaginiᚋg
return ec._AlbumResponse(ctx, sel, v) return ec._AlbumResponse(ctx, sel, v)
} }
func (ec *executionContext) marshalNAuthResponse2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResponse(ctx context.Context, sel ast.SelectionSet, v model.AuthResponse) graphql.Marshaler {
return ec._AuthResponse(ctx, sel, &v)
}
func (ec *executionContext) marshalNAuthResponse2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResponse(ctx context.Context, sel ast.SelectionSet, v *model.AuthResponse) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._AuthResponse(ctx, sel, v)
}
func (ec *executionContext) unmarshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx context.Context, v interface{}) (model.AuthResult, error) { func (ec *executionContext) unmarshalNAuthResult2reichardᚗioᚋimaginiᚋgraphᚋmodelᚐAuthResult(ctx context.Context, v interface{}) (model.AuthResult, error) {
var res model.AuthResult var res model.AuthResult
err := res.UnmarshalGQL(v) err := res.UnmarshalGQL(v)

View File

@ -1,12 +1,13 @@
package model package model
import ( import (
"net/http" "net/http"
"github.com/lestrrat-go/jwx/jwt"
) )
type AuthContext struct { type AuthContext struct {
AccessToken string AccessToken *jwt.Token
RefreshToken string AuthResponse *http.ResponseWriter
AuthResponse *http.ResponseWriter AuthRequest *http.Request
AuthRequest *http.Request
} }

View File

@ -1,36 +1,36 @@
package model package model
import ( import (
"gorm.io/gorm" "github.com/google/uuid"
"github.com/google/uuid" "gorm.io/gorm"
) )
func (u *User) BeforeCreate(tx *gorm.DB) (err error) { func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
newID := uuid.New().String() newID := uuid.New().String()
u.ID = &newID u.ID = newID
return return
} }
func (a *Album) BeforeCreate(tx *gorm.DB) (err error) { func (a *Album) BeforeCreate(tx *gorm.DB) (err error) {
newID := uuid.New().String() newID := uuid.New().String()
a.ID = &newID a.ID = newID
return return
} }
func (m *MediaItem) BeforeCreate(tx *gorm.DB) (err error) { func (m *MediaItem) BeforeCreate(tx *gorm.DB) (err error) {
newID := uuid.New().String() newID := uuid.New().String()
m.ID = &newID m.ID = newID
return return
} }
func (t *Tag) BeforeCreate(tx *gorm.DB) (err error) { func (t *Tag) BeforeCreate(tx *gorm.DB) (err error) {
newID := uuid.New().String() newID := uuid.New().String()
t.ID = &newID t.ID = newID
return return
} }
func (d *Device) BeforeCreate(tx *gorm.DB) (err error) { func (d *Device) BeforeCreate(tx *gorm.DB) (err error) {
newID := uuid.New().String() newID := uuid.New().String()
d.ID = &newID d.ID = newID
return return
} }

View File

@ -12,7 +12,7 @@ import (
) )
type Album struct { type Album struct {
ID *string `json:"id" gorm:"primarykey;not null"` ID string `json:"id" gorm:"primaryKey;not null"`
CreatedAt *time.Time `json:"createdAt" ` CreatedAt *time.Time `json:"createdAt" `
UpdatedAt *time.Time `json:"updatedAt" ` UpdatedAt *time.Time `json:"updatedAt" `
Name string `json:"name" gorm:"unique;not null"` Name string `json:"name" gorm:"unique;not null"`
@ -34,6 +34,7 @@ type AlbumResponse struct {
type AuthResponse struct { type AuthResponse struct {
Result AuthResult `json:"Result" ` Result AuthResult `json:"Result" `
Device *Device `json:"Device" `
Error *string `json:"Error" ` Error *string `json:"Error" `
} }
@ -50,12 +51,13 @@ type BooleanFilter struct {
} }
type Device struct { type Device struct {
ID *string `json:"id" gorm:"primarykey;not null"` ID string `json:"id" gorm:"primaryKey;not null"`
CreatedAt *time.Time `json:"createdAt" ` CreatedAt *time.Time `json:"createdAt" `
UpdatedAt *time.Time `json:"updatedAt" ` UpdatedAt *time.Time `json:"updatedAt" `
Name string `json:"name" gorm:"not null"` Name string `json:"name" gorm:"not null"`
Type DeviceType `json:"type" gorm:"default:Unknown;not null"` Type DeviceType `json:"type" gorm:"default:Unknown;not null"`
User *User `json:"user" gorm:"ForeignKey:ID;not null"` UserID string `json:"userID" gorm:"not null"`
User *User `json:"user" gorm:"foreignKey:ID;references:UserID;not null"`
RefreshKey *string `json:"refreshKey" ` RefreshKey *string `json:"refreshKey" `
} }
@ -111,7 +113,7 @@ type IntFilter struct {
} }
type MediaItem struct { type MediaItem struct {
ID *string `json:"id" gorm:"primarykey;not null"` ID string `json:"id" gorm:"primaryKey;not null"`
CreatedAt *time.Time `json:"createdAt" ` CreatedAt *time.Time `json:"createdAt" `
UpdatedAt *time.Time `json:"updatedAt" ` UpdatedAt *time.Time `json:"updatedAt" `
ExifDate *time.Time `json:"exifDate" ` ExifDate *time.Time `json:"exifDate" `
@ -122,7 +124,8 @@ type MediaItem struct {
OrigName string `json:"origName" gorm:"not null"` OrigName string `json:"origName" gorm:"not null"`
Tags []*Tag `json:"tags" gorm:"many2many:media_tags"` Tags []*Tag `json:"tags" gorm:"many2many:media_tags"`
Albums []*Album `json:"albums" gorm:"many2many:media_albums"` Albums []*Album `json:"albums" gorm:"many2many:media_albums"`
User *User `json:"user" gorm:"ForeignKey:ID;not null"` UserID string `json:"userID" gorm:"not null"`
User *User `json:"user" gorm:"foreignKey:ID;references:UserID;not null"`
} }
type MediaItemFilter struct { type MediaItemFilter struct {
@ -206,7 +209,7 @@ type StringFilter struct {
} }
type Tag struct { type Tag struct {
ID *string `json:"id" gorm:"primarykey;not null"` ID string `json:"id" gorm:"primaryKey;not null"`
CreatedAt *time.Time `json:"createdAt" ` CreatedAt *time.Time `json:"createdAt" `
UpdatedAt *time.Time `json:"updatedAt" ` UpdatedAt *time.Time `json:"updatedAt" `
Name string `json:"name" gorm:"unique;not null"` Name string `json:"name" gorm:"unique;not null"`
@ -236,7 +239,7 @@ type TimeFilter struct {
} }
type User struct { type User struct {
ID *string `json:"id" gorm:"primarykey;not null"` ID string `json:"id" gorm:"primaryKey;not null"`
CreatedAt *time.Time `json:"createdAt" ` CreatedAt *time.Time `json:"createdAt" `
UpdatedAt *time.Time `json:"updatedAt" ` UpdatedAt *time.Time `json:"updatedAt" `
Email string `json:"email" gorm:"not null;unique"` Email string `json:"email" gorm:"not null;unique"`

View File

@ -1,13 +1,15 @@
package graph package graph
import ( import (
"reichard.io/imagini/internal/db" "reichard.io/imagini/internal/auth"
"reichard.io/imagini/internal/db"
) )
// This file will not be regenerated automatically. // This file will not be regenerated automatically.
// //
// It serves as dependency injection for your app, add any dependencies you require here. // It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct{ type Resolver struct {
DB *db.DBManager Auth *auth.AuthManager
DB *db.DBManager
} }

View File

@ -42,6 +42,7 @@ enum AuthResult {
type AuthResponse { type AuthResponse {
Result: AuthResult! Result: AuthResult!
Device: Device
Error: String Error: String
} }
@ -138,17 +139,18 @@ input AuthTypeFilter {
# ------------------------------------------------------------ # ------------------------------------------------------------
type Device { type Device {
id: ID @meta(gorm: "primarykey;not null") id: ID! @meta(gorm: "primaryKey;not null")
createdAt: Time createdAt: Time
updatedAt: Time updatedAt: Time
name: String! @meta(gorm: "not null") name: String! @meta(gorm: "not null")
type: DeviceType! @meta(gorm: "default:Unknown;not null") type: DeviceType! @meta(gorm: "default:Unknown;not null")
user: User @meta(gorm: "ForeignKey:ID;not null") userID: ID! @meta(gorm: "not null")
user: User! @meta(gorm: "foreignKey:ID;references:UserID;not null")
refreshKey: String refreshKey: String
} }
type User { type User {
id: ID @meta(gorm: "primarykey;not null") id: ID! @meta(gorm: "primaryKey;not null")
createdAt: Time createdAt: Time
updatedAt: Time updatedAt: Time
email: String! @meta(gorm: "not null;unique") email: String! @meta(gorm: "not null;unique")
@ -161,7 +163,7 @@ type User {
} }
type MediaItem { type MediaItem {
id: ID @meta(gorm: "primarykey;not null") id: ID! @meta(gorm: "primaryKey;not null")
createdAt: Time createdAt: Time
updatedAt: Time updatedAt: Time
exifDate: Time exifDate: Time
@ -172,18 +174,19 @@ type MediaItem {
origName: String! @meta(gorm: "not null") origName: String! @meta(gorm: "not null")
tags: [Tag] @meta(gorm: "many2many:media_tags") tags: [Tag] @meta(gorm: "many2many:media_tags")
albums: [Album] @meta(gorm: "many2many:media_albums") albums: [Album] @meta(gorm: "many2many:media_albums")
user: User @meta(gorm: "ForeignKey:ID;not null") userID: ID! @meta(gorm: "not null")
user: User! @meta(gorm: "foreignKey:ID;references:UserID;not null")
} }
type Tag { type Tag {
id: ID @meta(gorm: "primarykey;not null") id: ID! @meta(gorm: "primaryKey;not null")
createdAt: Time createdAt: Time
updatedAt: Time updatedAt: Time
name: String! @meta(gorm: "unique;not null") name: String! @meta(gorm: "unique;not null")
} }
type Album { type Album {
id: ID @meta(gorm: "primarykey;not null") id: ID! @meta(gorm: "primaryKey;not null")
createdAt: Time createdAt: Time
updatedAt: Time updatedAt: Time
name: String! @meta(gorm: "unique;not null") name: String! @meta(gorm: "unique;not null")
@ -331,8 +334,9 @@ type Query {
login( login(
user: String! user: String!
password: String! password: String!
): AuthResult! deviceID: ID
logout: AuthResult! @hasMinRole(role: User) ): AuthResponse!
logout: AuthResponse! @hasMinRole(role: User)
# Single Item # Single Item
mediaItem(id: ID!): MediaItem! @hasMinRole(role: User) mediaItem(id: ID!): MediaItem! @hasMinRole(role: User)

View File

@ -4,10 +4,12 @@ package graph
// will be copied through when generating and any unknown code will be moved to the end. // will be copied through when generating and any unknown code will be moved to the end.
import ( import (
"net/http"
"context" "context"
"fmt" "fmt"
"net/http"
"strings"
"github.com/google/uuid"
"reichard.io/imagini/graph/generated" "reichard.io/imagini/graph/generated"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
) )
@ -41,26 +43,66 @@ func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser)
err := r.DB.CreateUser(user) err := r.DB.CreateUser(user)
if err != nil { if err != nil {
panic(fmt.Errorf("DB Error")) return nil, err
} }
return user, nil return user, nil
} }
func (r *queryResolver) Login(ctx context.Context, user string, password string) (model.AuthResult, error) { func (r *queryResolver) Login(ctx context.Context, user string, password string, deviceID *string) (*model.AuthResponse, error) {
// Set Cookie From Context
// Set Cookie From Context
authContext := ctx.Value("auth").(*model.AuthContext) authContext := ctx.Value("auth").(*model.AuthContext)
resp := *authContext.AuthResponse resp := authContext.AuthResponse
testCookie := http.Cookie{Name: "TestCookie", Value: "Test123", Path: "/", HttpOnly: true} req := authContext.AuthRequest
http.SetCookie(resp, &testCookie)
return model.AuthResultSuccess, nil // Do Login
foundUser, success := r.Auth.AuthenticateUser(user, password)
if !success {
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
}
// Upsert Device
foundDevice := model.Device{}
if deviceID != nil {
parsedDeviceID, err := uuid.Parse(*deviceID)
if err != nil {
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
}
foundDevice.ID = parsedDeviceID.String()
count, err := r.DB.Device(&foundDevice)
if count != 1 || err != nil {
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
}
} else {
foundDevice.Type = deriveDeviceType(req)
err := r.DB.CreateDevice(&foundDevice)
if err != nil {
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
}
}
// Create Tokens
accessToken, err := r.Auth.CreateJWTAccessToken(foundUser, foundDevice)
if err != nil {
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
}
refreshToken, err := r.Auth.CreateJWTRefreshToken(foundUser, foundDevice)
if err != nil {
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
}
// Set appropriate cookies
accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken, Path: "/", HttpOnly: true}
refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken, Path: "/", HttpOnly: true}
http.SetCookie(*resp, &accessCookie)
http.SetCookie(*resp, &refreshCookie)
return &model.AuthResponse{Result: model.AuthResultSuccess, Device: &foundDevice}, nil
} }
func (r *queryResolver) Logout(ctx context.Context) (model.AuthResult, error) { func (r *queryResolver) Logout(ctx context.Context) (*model.AuthResponse, error) {
// panic(fmt.Errorf("not implemented")) // panic(fmt.Errorf("not implemented"))
return model.AuthResultSuccess, nil return &model.AuthResponse{Result: model.AuthResultSuccess}, nil
} }
func (r *queryResolver) MediaItem(ctx context.Context, id string) (*model.MediaItem, error) { func (r *queryResolver) MediaItem(ctx context.Context, id string) (*model.MediaItem, error) {
@ -126,3 +168,29 @@ func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver } type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
func deriveDeviceType(r *http.Request) model.DeviceType {
userAgent := strings.ToLower(r.Header.Get("User-Agent"))
if strings.Contains(userAgent, "ios-imagini") {
return model.DeviceTypeIOs
} else if strings.Contains(userAgent, "android-imagini") {
return model.DeviceTypeAndroid
} else if strings.Contains(userAgent, "chrome") {
return model.DeviceTypeChrome
} else if strings.Contains(userAgent, "firefox") {
return model.DeviceTypeFirefox
} else if strings.Contains(userAgent, "msie") {
return model.DeviceTypeInternetExplorer
} else if strings.Contains(userAgent, "edge") {
return model.DeviceTypeEdge
} else if strings.Contains(userAgent, "safari") {
return model.DeviceTypeSafari
}
return model.DeviceTypeUnknown
}

View File

@ -1,248 +1,124 @@
package api package api
import ( import (
"fmt" "context"
"time" "errors"
"strings" "fmt"
"context" "net/http"
"net/http"
"encoding/json"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/lestrrat-go/jwx/jwt"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/jwt"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
) )
func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) { func (api *API) refreshTokens(refreshToken jwt.Token) (string, string, error) {
w.Header().Set("Access-Control-Allow-Origin", "*") // Acquire User & Device
if r.Method != http.MethodPost { did, ok := refreshToken.Get("did")
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed) if !ok {
return return "", "", errors.New("Missing DID")
} }
uid, ok := refreshToken.Get(jwt.SubjectKey)
if !ok {
return "", "", errors.New("Missing UID")
}
deviceUUID, err := uuid.Parse(fmt.Sprintf("%v", did))
if err != nil {
return "", "", errors.New("Invalid DID")
}
userUUID, err := uuid.Parse(fmt.Sprintf("%v", uid))
if err != nil {
return "", "", errors.New("Invalid UID")
}
// Decode into Struct // Device & User Skeleton
var creds APICredentials user := model.User{ID: userUUID.String()}
err := json.NewDecoder(r.Body).Decode(&creds) device := model.Device{ID: deviceUUID.String()}
if err != nil {
errorJSON(w, "Invalid parameters.", http.StatusBadRequest)
return
}
// Validate // Find User
if creds.User == "" || creds.Password == "" { _, err = api.DB.User(&user)
errorJSON(w, "Invalid parameters.", http.StatusBadRequest) if err != nil {
return return "", "", err
} }
// Do login // Update Access Token
resp, user := api.Auth.AuthenticateUser(creds.User, creds.Password) accessTokenCookie, err := api.Auth.CreateJWTAccessToken(user, device)
if !resp { if err != nil {
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return "", "", err
return }
}
// Upsert device return accessTokenCookie, "", err
device, err := api.upsertRequestedDevice(user, r)
if err != nil {
log.Error("[api] loginHandler - Failed to upsert device: ", err)
errorJSON(w, "DB error. Unable to proceed.", http.StatusUnauthorized)
return
}
// Create Tokens
accessToken, err := api.Auth.CreateJWTAccessToken(user, device)
refreshToken, err := api.Auth.CreateJWTRefreshToken(user, device)
// Set appropriate cookies
accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken, Path: "/", HttpOnly: true}
refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken, Path: "/", HttpOnly: true}
http.SetCookie(w, &accessCookie)
http.SetCookie(w, &refreshCookie)
// Response success
successJSON(w, "Login success.", http.StatusOK)
} }
func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) { func (api *API) validateTokens(w *http.ResponseWriter, r *http.Request) (jwt.Token, error) {
if r.Method != http.MethodPost { // Validate Access Token
http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed) accessCookie, _ := r.Cookie("AccessToken")
return if accessCookie != nil {
} accessToken, err := api.Auth.ValidateJWTAccessToken(accessCookie.Value)
if err == nil {
return accessToken, nil
}
}
// TODO: Reset Refresh Key // Validate Refresh Cookie Exists
refreshCookie, _ := r.Cookie("RefreshToken")
if refreshCookie == nil {
return nil, errors.New("Tokens Invalid")
}
// Clear Cookies // Validate Refresh Token
http.SetCookie(w, &http.Cookie{Name: "AccessToken", Expires: time.Unix(0, 0)}) refreshToken, err := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value)
http.SetCookie(w, &http.Cookie{Name: "RefreshToken", Expires: time.Unix(0, 0)}) if err != nil {
return nil, errors.New("Tokens Invalid")
}
successJSON(w, "Logout success.", http.StatusOK) // Refresh Access Token & Generate New Refresh Token
newAccessCookie, newRefreshCookie, err := api.refreshTokens(refreshToken)
if err != nil {
return nil, err
}
// TODO: Actually Refresh Refresh Token
newRefreshCookie = refreshCookie.Value
// Update Access & Refresh Cookies
http.SetCookie(*w, &http.Cookie{
Name: "AccessToken",
Value: newAccessCookie,
})
http.SetCookie(*w, &http.Cookie{
Name: "RefreshToken",
Value: newRefreshCookie,
})
return jwt.ParseBytes([]byte(newAccessCookie))
} }
/** func (api *API) hasMinRoleDirective(ctx context.Context, obj interface{}, next graphql.Resolver, role model.Role) (res interface{}, err error) {
* This will find or create the requested device based on ID and User. authContext := ctx.Value("auth").(*model.AuthContext)
**/ accessToken, err := api.validateTokens(authContext.AuthResponse, authContext.AuthRequest)
func (api *API) upsertRequestedDevice(user model.User, r *http.Request) (model.Device, error) { if err != nil {
requestedDevice := deriveRequestedDevice(r) return nil, errors.New("Access Denied")
requestedDevice.Type = deriveDeviceType(r) }
requestedDevice.User.ID = user.ID authContext.AccessToken = &accessToken
if *requestedDevice.ID == "" { userRole, ok := accessToken.Get("role")
err := api.DB.CreateDevice(&requestedDevice) if !ok {
createdDevice, err := api.DB.Device(&requestedDevice) return nil, errors.New("Access Denied")
return createdDevice, err }
}
foundDevice, err := api.DB.Device(&model.Device{ if userRole == model.RoleAdmin.String() {
ID: requestedDevice.ID, return next(ctx)
User: &user, }
})
return foundDevice, err if userRole == role.String() {
return next(ctx)
}
return nil, errors.New("Role Not Authenticated")
} }
func deriveDeviceType(r *http.Request) model.DeviceType { func (api *API) metaDirective(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string) (res interface{}, err error) {
userAgent := strings.ToLower(r.Header.Get("User-Agent")) return next(ctx)
if strings.HasPrefix(userAgent, "ios-imagini"){
return model.DeviceTypeIOs
} else if strings.HasPrefix(userAgent, "android-imagini"){
return model.DeviceTypeAndroid
} else if strings.HasPrefix(userAgent, "chrome"){
return model.DeviceTypeChrome
} else if strings.HasPrefix(userAgent, "firefox"){
return model.DeviceTypeFirefox
} else if strings.HasPrefix(userAgent, "msie"){
return model.DeviceTypeInternetExplorer
} else if strings.HasPrefix(userAgent, "edge"){
return model.DeviceTypeEdge
} else if strings.HasPrefix(userAgent, "safari"){
return model.DeviceTypeSafari
}
return model.DeviceTypeUnknown
}
func deriveRequestedDevice(r *http.Request) model.Device {
deviceSkeleton := model.Device{}
authHeader := r.Header.Get("X-Imagini-Authorization")
splitAuthInfo := strings.Split(authHeader, ",")
// For each Key - Value pair
for i := range splitAuthInfo {
// Split Key - Value
item := strings.TrimSpace(splitAuthInfo[i])
splitItem := strings.SplitN(item, "=", 2)
if len(splitItem) != 2 {
continue
}
// Derive Key
key := strings.ToLower(strings.TrimSpace(splitItem[0]))
if key != "deviceid" && key != "devicename" {
continue
}
// Derive Value
val := trimQuotes(strings.TrimSpace(splitItem[1]))
if key == "deviceid" {
parsedDeviceUUID, err := uuid.Parse(val)
if err != nil {
log.Warn("[auth] deriveRequestedDevice - Unable to parse requested DeviceUUID: ", val)
continue
}
stringDeviceUUID := parsedDeviceUUID.String()
deviceSkeleton.ID = &stringDeviceUUID
} else if key == "devicename" {
deviceSkeleton.Name = val
}
}
// If name not set, set to type
if deviceSkeleton.Name == "" {
deviceSkeleton.Name = deviceSkeleton.Type.String()
}
return deviceSkeleton
}
func (api *API) refreshAccessToken(w http.ResponseWriter, r *http.Request) (jwt.Token, error) {
refreshCookie, err := r.Cookie("RefreshToken")
if err != nil {
log.Warn("[middleware] RefreshToken not found")
return nil, err
}
// Validate Refresh Token
refreshToken, err := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value)
if err != nil {
http.SetCookie(w, &http.Cookie{Name: "AccessToken", Expires: time.Unix(0, 0)})
http.SetCookie(w, &http.Cookie{Name: "RefreshToken", Expires: time.Unix(0, 0)})
return nil, err
}
// Acquire User & Device (Trusted)
did, ok := refreshToken.Get("did")
if !ok {
return nil, err
}
uid, ok := refreshToken.Get(jwt.SubjectKey)
if !ok {
return nil, err
}
deviceUUID, err := uuid.Parse(fmt.Sprintf("%v", did))
if err != nil {
return nil, err
}
userUUID, err := uuid.Parse(fmt.Sprintf("%v", uid))
if err != nil {
return nil, err
}
stringUserUUID := userUUID.String()
stringDeviceUUID := deviceUUID.String()
// Device & User Skeleton
user := model.User{ID: &stringUserUUID}
device := model.Device{ID: &stringDeviceUUID}
// Update token
accessTokenString, err := api.Auth.CreateJWTAccessToken(user, device)
if err != nil {
return nil, err
}
accessCookie := http.Cookie{Name: "AccessToken", Value: accessTokenString}
http.SetCookie(w, &accessCookie)
// TODO: Update Refresh Key & Token
// Convert to jwt.Token
accessTokenBytes := []byte(accessTokenString)
accessToken, err := jwt.ParseBytes(accessTokenBytes)
return accessToken, err
}
func trimQuotes(s string) string {
if len(s) >= 2 {
if s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
}
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)
} }

View File

@ -1,50 +1,53 @@
package api package api
import ( import (
"os"
"path"
"net/http" "net/http"
"os"
"path"
"reichard.io/imagini/graph/model"
) )
// Responsible for serving up static images / videos // Responsible for serving up static images / videos
func (api *API) mediaHandler(w http.ResponseWriter, r *http.Request) { func (api *API) mediaHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return return
} }
if path.Dir(r.URL.Path) != "/media" { if path.Dir(r.URL.Path) != "/media" {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return return
} }
// Acquire Width & Height Parameters // Acquire Width & Height Parameters
query := r.URL.Query() query := r.URL.Query()
width := query["width"] width := query["width"]
height := query["height"] height := query["height"]
_ = width _ = width
_ = height _ = height
// TODO: Caching & Resizing // TODO: Caching & Resizing
// - If both, force resize with new scale // - If both, force resize with new scale
// - If one, scale resize proportionally // - If one, scale resize proportionally
// Pull out UUIDs // Pull out userID
reqInfo := r.Context().Value("uuids").(map[string]string) authContext := r.Context().Value("auth").(*model.AuthContext)
uid := reqInfo["uid"] rawUserID, _ := (*authContext.AccessToken).Get("sub")
userID := rawUserID.(string)
// Derive Path // Derive Path
fileName := path.Base(r.URL.Path) fileName := path.Base(r.URL.Path)
folderPath := path.Join("/" + api.Config.DataPath + "/media/" + uid) folderPath := path.Join("/" + api.Config.DataPath + "/media/" + userID)
mediaPath := path.Join(folderPath + "/" + fileName) mediaPath := path.Join(folderPath + "/" + fileName)
// Check if File Exists // Check if File Exists
_, err := os.Stat(mediaPath) _, err := os.Stat(mediaPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
// TODO: Different HTTP Response Code? // TODO: Different HTTP Response Code?
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return return
} }
http.ServeFile(w, r, mediaPath) http.ServeFile(w, r, mediaPath)
} }

View File

@ -1,104 +1,79 @@
package api package api
import ( import (
log "github.com/sirupsen/logrus" "context"
"net/http" "net/http"
"context" "os"
"os"
"reichard.io/imagini/graph/model" log "github.com/sirupsen/logrus"
"reichard.io/imagini/graph/model"
) )
type Middleware func(http.Handler) http.HandlerFunc type Middleware func(http.Handler) http.HandlerFunc
func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc { func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc {
if len(m) < 1 { if len(m) < 1 {
return h return h
} }
wrapped := h wrapped := h
for i := len(m) - 1; i >= 0; i-- { for i := len(m) - 1; i >= 0; i-- {
wrapped = m[i](wrapped) wrapped = m[i](wrapped)
} }
return wrapped return wrapped
} }
/**
* This is used for the graphQL endpoints that may require access to the
* Request and ResponseWriter variables. These are used to get / set cookies.
**/
func (api *API) injectContextMiddleware(next http.Handler) http.Handler { func (api *API) injectContextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 authContext := &model.AuthContext{
ctx := context.WithValue(r.Context(), "auth", authContext) AuthResponse: &w,
r = r.WithContext(ctx) AuthRequest: r,
}
log.Info("[middleware] Exiting testMiddleware...") // Add context
next.ServeHTTP(w, r) ctx := context.WithValue(r.Context(), "auth", authContext)
}) r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
} }
/**
* This is used for non graphQL endpoints that require authentication.
**/
func (api *API) authMiddleware(next http.Handler) http.HandlerFunc { func (api *API) authMiddleware(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Acquire Token accessToken, err := api.validateTokens(&w, r)
accessCookie, err := r.Cookie("AccessToken") if err != nil {
if err != nil { errorJSON(w, "Invalid token.", http.StatusUnauthorized)
log.Warn("[middleware] AccessToken not found") return
errorJSON(w, "Invalid token.", http.StatusUnauthorized) }
return
}
// Validate JWT Tokens // Create Context
accessToken, err := api.Auth.ValidateJWTAccessToken(accessCookie.Value) authContext := &model.AuthContext{
AccessToken: &accessToken,
}
ctx := context.WithValue(r.Context(), "auth", authContext)
r = r.WithContext(ctx)
if err != nil && err.Error() == "exp not satisfied" { next.ServeHTTP(w, r)
log.Info("[middleware] Refreshing AccessToken")
accessToken, err = api.refreshAccessToken(w, r)
if err != nil {
log.Warn("[middleware] Refreshing AccessToken failed: ", err)
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
return
}
log.Info("[middleware] AccessToken Refreshed")
} else if err != nil {
log.Warn("[middleware] AccessToken failed to validate")
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
return
}
// Acquire UserID and DeviceID })
reqInfo := make(map[string]string)
uid, _ := accessToken.Get("sub")
did, _ := accessToken.Get("did")
reqInfo["uid"] = uid.(string)
reqInfo["did"] = did.(string)
// Add context
ctx := context.WithValue(r.Context(), "uuids", reqInfo)
sr := r.WithContext(ctx)
next.ServeHTTP(w, sr)
})
} }
func (api *API) logMiddleware(h http.Handler) http.Handler { func (api *API) logMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.SetOutput(os.Stdout)
log.Println(r.Method, r.URL) log.SetOutput(os.Stdout)
h.ServeHTTP(w, r) log.Println(r.Method, r.URL)
}) h.ServeHTTP(w, r)
})
} }

View File

@ -1,8 +1,8 @@
package api package api
import ( import (
"net/http" "encoding/json"
"encoding/json" "net/http"
"github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground" "github.com/99designs/gqlgen/graphql/playground"
@ -13,39 +13,46 @@ import (
func (api *API) registerRoutes() { func (api *API) registerRoutes() {
// Set up Directives // Set up Directives
c := generated.Config{ Resolvers: &graph.Resolver{ DB: api.DB } } graphConfig := generated.Config{
c.Directives.HasMinRole = hasMinRoleDirective Resolvers: &graph.Resolver{
c.Directives.Meta = metaDirective DB: api.DB,
srv := handler.NewDefaultServer(generated.NewExecutableSchema(c)) Auth: api.Auth,
},
Directives: generated.DirectiveRoot{
Meta: api.metaDirective,
HasMinRole: api.hasMinRoleDirective,
},
}
srv := handler.NewDefaultServer(generated.NewExecutableSchema(graphConfig))
// Handle GraphQL // Handle GraphQL
api.Router.Handle("/playground", playground.Handler("GraphQL playground", "/query")) api.Router.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
api.Router.Handle("/query", api.injectContextMiddleware(srv)) api.Router.Handle("/query", api.injectContextMiddleware(srv))
// Handle Resource Route // Handle Resource Route
api.Router.HandleFunc("/media/", multipleMiddleware( api.Router.HandleFunc("/media/", multipleMiddleware(
api.mediaHandler, api.mediaHandler,
api.authMiddleware, api.authMiddleware,
)) ))
} }
func errorJSON(w http.ResponseWriter, err string, code int) { func errorJSON(w http.ResponseWriter, err string, code int) {
errStruct := &APIResponse{Error: &APIError{Message: err, Code: int64(code)}} errStruct := &APIResponse{Error: &APIError{Message: err, Code: int64(code)}}
responseJSON(w, errStruct, code) responseJSON(w, errStruct, code)
} }
func responseJSON(w http.ResponseWriter, msg interface{}, code int) { func responseJSON(w http.ResponseWriter, msg interface{}, code int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(code) w.WriteHeader(code)
json.NewEncoder(w).Encode(msg) json.NewEncoder(w).Encode(msg)
} }
func successJSON(w http.ResponseWriter, msg string, code int) { func successJSON(w http.ResponseWriter, msg string, code int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(code) w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]interface{}{"success": msg}) json.NewEncoder(w).Encode(map[string]interface{}{"success": msg})
} }

View File

@ -1,185 +1,175 @@
package auth package auth
import ( import (
"fmt" "encoding/json"
"time" "errors"
"errors" "fmt"
"encoding/json" "time"
"gorm.io/gorm" "github.com/google/uuid"
"github.com/google/uuid" "github.com/lestrrat-go/jwx/jwa"
log "github.com/sirupsen/logrus" "github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/jwa" log "github.com/sirupsen/logrus"
"github.com/lestrrat-go/jwx/jwt" "gorm.io/gorm"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
"reichard.io/imagini/internal/db" "reichard.io/imagini/internal/config"
"reichard.io/imagini/internal/config" "reichard.io/imagini/internal/db"
"reichard.io/imagini/internal/session"
) )
type AuthManager struct { type AuthManager struct {
DB *db.DBManager DB *db.DBManager
Config *config.Config Config *config.Config
Session *session.SessionManager
} }
func NewMgr(db *db.DBManager, c *config.Config) *AuthManager { func NewMgr(db *db.DBManager, c *config.Config) *AuthManager {
session := session.NewMgr() return &AuthManager{
return &AuthManager{ DB: db,
DB: db, Config: c,
Config: c, }
Session: session,
}
} }
func (auth *AuthManager) AuthenticateUser(user, password string) (bool, model.User) { func (auth *AuthManager) AuthenticateUser(user, password string) (model.User, bool) {
// Search Objects // Find User by Username / Email
userByName := &model.User{} foundUser := &model.User{Username: user}
userByName.Username = user _, err := auth.DB.User(foundUser)
foundUser, err := auth.DB.User(userByName) // By Username
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
userByEmail := &model.User{} foundUser = &model.User{Email: user}
userByEmail.Email = user _, err = auth.DB.User(foundUser)
foundUser, err = auth.DB.User(userByEmail) }
}
// Error Checking // By Email
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn("[auth] User not found: ", user) log.Warn("[auth] User not found: ", user)
return false, foundUser return *foundUser, false
} else if err != nil { } else if err != nil {
log.Error(err) log.Error(err)
return false, foundUser return *foundUser, false
} }
log.Info("[auth] Authenticating user: ", foundUser.Username) log.Info("[auth] Authenticating user: ", foundUser.Username)
// Determine Type // Determine Type
switch foundUser.AuthType { switch foundUser.AuthType {
case "Local": case "Local":
return authenticateLocalUser(foundUser, password), foundUser return *foundUser, authenticateLocalUser(*foundUser, password)
case "LDAP": case "LDAP":
return authenticateLDAPUser(foundUser, password), foundUser return *foundUser, authenticateLDAPUser(*foundUser, password)
default: default:
return false, foundUser return *foundUser, false
} }
}
func (auth *AuthManager) getRole(user model.User) string {
// TODO: Lookup role of user
return "User"
} }
func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, error) { func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, error) {
byteRefreshJWT := []byte(refreshJWT) byteRefreshJWT := []byte(refreshJWT)
// Acquire Relevant Device // Acquire Relevant Device
unverifiedToken, err := jwt.ParseBytes(byteRefreshJWT) unverifiedToken, err := jwt.ParseBytes(byteRefreshJWT)
did, ok := unverifiedToken.Get("did") did, ok := unverifiedToken.Get("did")
if !ok { if !ok {
return nil, errors.New("did does not exist") return nil, errors.New("did does not exist")
} }
deviceID, err := uuid.Parse(fmt.Sprintf("%v", did)) deviceID, err := uuid.Parse(fmt.Sprintf("%v", did))
if err != nil { if err != nil {
return nil, errors.New("did does not parse") return nil, errors.New("did does not parse")
} }
stringDeviceID := deviceID.String() device := &model.Device{ID: deviceID.String()}
device, err := auth.DB.Device(&model.Device{ID: &stringDeviceID}) _, err = auth.DB.Device(device)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Verify & Validate Token // Verify & Validate Token
verifiedToken, err := jwt.ParseBytes(byteRefreshJWT, verifiedToken, err := jwt.ParseBytes(byteRefreshJWT,
jwt.WithValidate(true), jwt.WithValidate(true),
jwt.WithVerify(jwa.HS256, []byte(*device.RefreshKey)), jwt.WithVerify(jwa.HS256, []byte(*device.RefreshKey)),
) )
if err != nil { if err != nil {
fmt.Println("failed to parse payload: ", err) fmt.Println("failed to parse payload: ", err)
return nil, err return nil, err
} }
return verifiedToken, nil return verifiedToken, nil
} }
func (auth *AuthManager) ValidateJWTAccessToken(accessJWT string) (jwt.Token, error) { func (auth *AuthManager) ValidateJWTAccessToken(accessJWT string) (jwt.Token, error) {
byteAccessJWT := []byte(accessJWT) byteAccessJWT := []byte(accessJWT)
verifiedToken, err := jwt.ParseBytes(byteAccessJWT, verifiedToken, err := jwt.ParseBytes(byteAccessJWT,
jwt.WithValidate(true), jwt.WithValidate(true),
jwt.WithVerify(jwa.HS256, []byte(auth.Config.JWTSecret)), jwt.WithVerify(jwa.HS256, []byte(auth.Config.JWTSecret)),
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
return verifiedToken, nil return verifiedToken, nil
} }
func (auth *AuthManager) CreateJWTRefreshToken(user model.User, device model.Device) (string, error) { func (auth *AuthManager) CreateJWTRefreshToken(user model.User, device model.Device) (string, error) {
// Acquire Refresh Key // Acquire Refresh Key
byteKey := []byte(*device.RefreshKey) byteKey := []byte(*device.RefreshKey)
// Create New Token // Create New Token
tm := time.Now() tm := time.Now()
t := jwt.New() t := jwt.New()
t.Set(`did`, device.ID) // Device ID t.Set(`did`, device.ID) // Device ID
t.Set(jwt.SubjectKey, user.ID) // User ID t.Set(jwt.SubjectKey, user.ID) // User ID
t.Set(jwt.AudienceKey, `imagini`) // App ID t.Set(jwt.AudienceKey, `imagini`) // App ID
t.Set(jwt.IssuedAtKey, tm) // Issued At t.Set(jwt.IssuedAtKey, tm) // Issued At
// iOS & Android = Never Expiring Refresh Token // iOS & Android = Never Expiring Refresh Token
if device.Type != "iOS" && device.Type != "Android" { if device.Type != "iOS" && device.Type != "Android" {
t.Set(jwt.ExpirationKey, tm.Add(time.Hour * 24)) // 1 Day Access Key t.Set(jwt.ExpirationKey, tm.Add(time.Hour*24)) // 1 Day Access Key
} }
// Validate Token Creation // Validate Token Creation
_, err := json.MarshalIndent(t, "", " ") _, err := json.MarshalIndent(t, "", " ")
if err != nil { if err != nil {
fmt.Printf("failed to generate JSON: %s\n", err) fmt.Printf("failed to generate JSON: %s\n", err)
return "", err return "", err
} }
// Sign Token // Sign Token
signed, err := jwt.Sign(t, jwa.HS256, byteKey) signed, err := jwt.Sign(t, jwa.HS256, byteKey)
if err != nil { if err != nil {
log.Printf("failed to sign token: %s", err) log.Printf("failed to sign token: %s", err)
return "", err return "", err
} }
// Return Token // Return Token
return string(signed), nil return string(signed), nil
} }
func (auth *AuthManager) CreateJWTAccessToken(user model.User, device model.Device) (string, error) { func (auth *AuthManager) CreateJWTAccessToken(user model.User, device model.Device) (string, error) {
// Create New Token // Create New Token
tm := time.Now() tm := time.Now()
t := jwt.New() t := jwt.New()
t.Set(`did`, device.ID) // Device ID t.Set(`did`, device.ID) // Device ID
t.Set(`role`, auth.getRole(user)) // User Role (Admin / User) t.Set(`role`, user.Role.String()) // User Role (Admin / User)
t.Set(jwt.SubjectKey, user.ID) // User ID t.Set(jwt.SubjectKey, user.ID) // User ID
t.Set(jwt.AudienceKey, `imagini`) // App ID t.Set(jwt.AudienceKey, `imagini`) // App ID
t.Set(jwt.IssuedAtKey, tm) // Issued At t.Set(jwt.IssuedAtKey, tm) // Issued At
t.Set(jwt.ExpirationKey, tm.Add(time.Hour * 2)) // 2 Hour Access Key t.Set(jwt.ExpirationKey, tm.Add(time.Hour*2)) // 2 Hour Access Key
// Validate Token Creation // Validate Token Creation
_, err := json.MarshalIndent(t, "", " ") _, err := json.MarshalIndent(t, "", " ")
if err != nil { if err != nil {
fmt.Printf("failed to generate JSON: %s\n", err) fmt.Printf("failed to generate JSON: %s\n", err)
return "", err return "", err
} }
// Use Server Key // Use Server Key
byteKey := []byte(auth.Config.JWTSecret) byteKey := []byte(auth.Config.JWTSecret)
// Sign Token // Sign Token
signed, err := jwt.Sign(t, jwa.HS256, byteKey) signed, err := jwt.Sign(t, jwa.HS256, byteKey)
if err != nil { if err != nil {
log.Printf("failed to sign token: %s", err) log.Printf("failed to sign token: %s", err)
return "", err return "", err
} }
// Return Token // Return Token
return string(signed), nil return string(signed), nil
} }

View File

@ -1,113 +1,113 @@
package db package db
import ( import (
"errors" "errors"
"path" "fmt"
"fmt" "path"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
// "gorm.io/gorm/logger" // "gorm.io/gorm/logger"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"reichard.io/imagini/internal/config" "reichard.io/imagini/graph/model"
"reichard.io/imagini/graph/model" "reichard.io/imagini/internal/config"
) )
type DBManager struct { type DBManager struct {
db *gorm.DB db *gorm.DB
} }
func NewMgr(c *config.Config) *DBManager { func NewMgr(c *config.Config) *DBManager {
gormConfig := &gorm.Config{ gormConfig := &gorm.Config{
PrepareStmt: true, PrepareStmt: true,
// Logger: logger.Default.LogMode(logger.Silent), // Logger: logger.Default.LogMode(logger.Silent),
} }
// Create manager // Create manager
dbm := &DBManager{} dbm := &DBManager{}
if c.DBType == "SQLite" { if c.DBType == "SQLite" {
dbLocation := path.Join(c.ConfigPath, "imagini.db") dbLocation := path.Join(c.ConfigPath, "imagini.db")
dbm.db, _ = gorm.Open(sqlite.Open(dbLocation), gormConfig) dbm.db, _ = gorm.Open(sqlite.Open(dbLocation), gormConfig)
} else { } else {
log.Fatal("Unsupported Database") log.Fatal("Unsupported Database")
} }
// Initialize database // Initialize database
dbm.db.AutoMigrate(&model.Device{}) dbm.db.AutoMigrate(&model.Device{})
dbm.db.AutoMigrate(&model.User{}) dbm.db.AutoMigrate(&model.User{})
dbm.db.AutoMigrate(&model.MediaItem{}) dbm.db.AutoMigrate(&model.MediaItem{})
dbm.db.AutoMigrate(&model.Tag{}) dbm.db.AutoMigrate(&model.Tag{})
dbm.db.AutoMigrate(&model.Album{}) dbm.db.AutoMigrate(&model.Album{})
// Determine whether to bootstrap // Determine whether to bootstrap
var count int64 var count int64
dbm.db.Model(&model.User{}).Count(&count) dbm.db.Model(&model.User{}).Count(&count)
if count == 0 { if count == 0 {
dbm.bootstrapDatabase() dbm.bootstrapDatabase()
} }
return dbm return dbm
} }
func (dbm *DBManager) bootstrapDatabase() { func (dbm *DBManager) bootstrapDatabase() {
log.Info("[query] Bootstrapping database.") log.Info("[query] Bootstrapping database.")
password := "admin" password := "admin"
user := &model.User{ user := &model.User{
Username: "admin", Username: "admin",
AuthType: "Local", AuthType: "Local",
Password: &password, Password: &password,
Role: model.RoleAdmin, Role: model.RoleAdmin,
} }
err := dbm.CreateUser(user) err := dbm.CreateUser(user)
if err != nil { if err != nil {
log.Fatal("[query] Unable to bootstrap database.") log.Fatal("[query] Unable to bootstrap database.")
} }
} }
func (dbm *DBManager) QueryBuilder(dest interface{}, params []byte) (int64, error) { func (dbm *DBManager) QueryBuilder(dest interface{}, params []byte) (int64, error) {
// TODO: // TODO:
// - Where Filters // - Where Filters
// - Sort Filters // - Sort Filters
// - Paging Filters // - Paging Filters
objType := fmt.Sprintf("%T", dest) objType := fmt.Sprintf("%T", dest)
if objType == "*[]model.MediaItem" { if objType == "*[]model.MediaItem" {
// TODO: Validate MediaItem Type // TODO: Validate MediaItem Type
} else { } else {
// Return Error // Return Error
return 0, errors.New("Invalid type") return 0, errors.New("Invalid type")
} }
var count int64 var count int64
err := dbm.db.Find(dest).Count(&count).Error; err := dbm.db.Find(dest).Count(&count).Error
return count, err return count, err
// Paging: // Paging:
// - Regular Pagination: // - Regular Pagination:
// - /api/v1/MediaItems?page[limit]=50&page=2 // - /api/v1/MediaItems?page[limit]=50&page=2
// - Meta Count Only // - Meta Count Only
// - /api/v1/MediaItems?page[limit]=0 // - /api/v1/MediaItems?page[limit]=0
// Sorting: // Sorting:
// - Ascending Sort: // - Ascending Sort:
// - /api/v1/MediaItems?sort=created_at // - /api/v1/MediaItems?sort=created_at
// - Descending Sort: // - Descending Sort:
// - /api/v1/MediaItems?sort=-created_at // - /api/v1/MediaItems?sort=-created_at
// Filters: // Filters:
// - Greater Than / Less Than (created_at, updated_at, exif_date) // - Greater Than / Less Than (created_at, updated_at, exif_date)
// - /api/v1/MediaItems?filter[created_at]>=2020-01-01&filter[created_at]<=2021-01-01 // - /api/v1/MediaItems?filter[created_at]>=2020-01-01&filter[created_at]<=2021-01-01
// - Long / Lat Range (latitude, longitude) // - Long / Lat Range (latitude, longitude)
// - /api/v1/MediaItems?filter[latitude]>=71.1827&filter[latitude]<=72.0000&filter[longitude]>=100.000&filter[longitude]<=101.0000 // - /api/v1/MediaItems?filter[latitude]>=71.1827&filter[latitude]<=72.0000&filter[longitude]>=100.000&filter[longitude]<=101.0000
// - Image / Video (media_type) // - Image / Video (media_type)
// - /api/v1/MediaItems?filter[media_type]=Image // - /api/v1/MediaItems?filter[media_type]=Image
// - Tags (tags) // - Tags (tags)
// - /api/v1/MediaItems?filter[tags]=id1,id2,id3 // - /api/v1/MediaItems?filter[tags]=id1,id2,id3
// - Albums (albums) // - Albums (albums)
// - /api/v1/MediaItems?filter[albums]=id1 // - /api/v1/MediaItems?filter[albums]=id1
} }

View File

@ -1,31 +1,30 @@
package db package db
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
) )
func (dbm *DBManager) CreateDevice (device *model.Device) error { func (dbm *DBManager) CreateDevice(device *model.Device) error {
log.Info("[db] Creating device: ", device.Name) log.Debug("[db] Creating device: ", device.Name)
refreshKey := uuid.New().String() refreshKey := uuid.New().String()
device.RefreshKey = &refreshKey device.RefreshKey = &refreshKey
err := dbm.db.Create(&device).Error err := dbm.db.Create(device).Error
return err return err
} }
func (dbm *DBManager) Device (device *model.Device) (model.Device, error) { func (dbm *DBManager) Device(device *model.Device) (int64, error) {
var foundDevice model.Device var count int64
var count int64 err := dbm.db.Where(device).First(device).Count(&count).Error
err := dbm.db.Where(&device).First(&foundDevice).Count(&count).Error return count, err
return foundDevice, err
} }
func (dbm *DBManager) DeleteDevice (user *model.Device) error { func (dbm *DBManager) DeleteDevice(user *model.Device) error {
return nil return nil
} }
func (dbm *DBManager) UpdateRefreshToken (device *model.Device, refreshToken string) error { func (dbm *DBManager) UpdateRefreshToken(device *model.Device, refreshToken string) error {
return nil return nil
} }

View File

@ -3,5 +3,5 @@ package db
import "errors" import "errors"
var ( var (
ErrUserAlreadyExists = errors.New("user already exists") ErrUserAlreadyExists = errors.New("user already exists")
) )

View File

@ -1,21 +1,21 @@
package db package db
import ( import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
) )
func (dbm *DBManager) CreateMediaItem (mediaItem *model.MediaItem) error { func (dbm *DBManager) CreateMediaItem(mediaItem *model.MediaItem) error {
log.Info("[db] Creating media item: ", mediaItem.FileName) log.Debug("[db] Creating media item: ", mediaItem.FileName)
err := dbm.db.Create(&mediaItem).Error err := dbm.db.Create(mediaItem).Error
return err return err
} }
func (dbm *DBManager) MediaItems(mediaItemFilter *model.MediaItem) ([]model.MediaItem, int64, error) { func (dbm *DBManager) MediaItems(mediaItemFilter *model.MediaItem) ([]model.MediaItem, int64, error) {
var mediaItems []model.MediaItem var mediaItems []model.MediaItem
var count int64 var count int64
err := dbm.db.Where(&mediaItemFilter).Find(&mediaItems).Count(&count).Error; err := dbm.db.Where(mediaItemFilter).Find(&mediaItems).Count(&count).Error
return mediaItems, count, err return mediaItems, count, err
} }

View File

@ -1,43 +1,41 @@
package db package db
import ( import (
"golang.org/x/crypto/bcrypt" log "github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt"
"reichard.io/imagini/graph/model" "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) log.Info("[db] Creating user: ", user.Username)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*user.Password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*user.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return err return err
} }
stringHashedPassword := string(hashedPassword) stringHashedPassword := string(hashedPassword)
user.Password = &stringHashedPassword user.Password = &stringHashedPassword
err = dbm.db.Create(&user).Error return dbm.db.Create(user).Error
return err
} }
func (dbm *DBManager) User (user *model.User) (model.User, error) { func (dbm *DBManager) User(user *model.User) (int64, error) {
var foundUser model.User var count int64
var count int64 err := dbm.db.Where(user).First(user).Count(&count).Error
err := dbm.db.Where(&user).First(&foundUser).Count(&count).Error return count, err
return foundUser, err
} }
func (dbm *DBManager) Users () ([]*model.User, int64, error) { func (dbm *DBManager) Users() ([]*model.User, int64, error) {
var foundUsers []*model.User var foundUsers []*model.User
var count int64 var count int64
err := dbm.db.Find(&foundUsers).Count(&count).Error err := dbm.db.Find(&foundUsers).Count(&count).Error
return foundUsers, count, err return foundUsers, count, err
} }
func (dbm *DBManager) DeleteUser (user model.User) error { func (dbm *DBManager) DeleteUser(user model.User) error {
return nil return nil
} }
func (dbm *DBManager) UpdatePassword (user model.User, pw string) { func (dbm *DBManager) UpdatePassword(user model.User, pw string) {
} }