diff --git a/api/go.mod b/api/go.mod index cb6c6db6..561e3824 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/99designs/gqlgen v0.10.2 + github.com/go-chi/chi v3.3.2+incompatible github.com/go-sql-driver/mysql v1.5.0 github.com/golang-migrate/migrate v3.5.4+incompatible github.com/joho/godotenv v1.3.0 diff --git a/api/go.sum b/api/go.sum index 563df734..f2f15411 100644 --- a/api/go.sum +++ b/api/go.sum @@ -5,6 +5,7 @@ github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4Rq github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= diff --git a/api/gqlgen.yml b/api/gqlgen.yml index 7cadfd6a..f350a941 100644 --- a/api/gqlgen.yml +++ b/api/gqlgen.yml @@ -13,6 +13,6 @@ resolver: filename: graphql/resolver.go type: Resolver autobind: [] -# models: -# Todo: -# model: github.com/viktorstrate/photoview/api.Todo +models: + User: + model: github.com/viktorstrate/photoview/api/graphql/models.User diff --git a/api/graphql/auth/auth.go b/api/graphql/auth/auth.go new file mode 100644 index 00000000..f724204a --- /dev/null +++ b/api/graphql/auth/auth.go @@ -0,0 +1,68 @@ +package auth + +import ( + "context" + "database/sql" + "errors" + "log" + "net/http" + "regexp" + + "github.com/viktorstrate/photoview/api/graphql/models" +) + +var ErrUnauthorized = errors.New("unauthorized") + +// A private key for context that only this package can access. This is important +// to prevent collisions between different context uses +var userCtxKey = &contextKey{"user"} + +type contextKey struct { + name string +} + +// Middleware decodes the share session cookie and packs the session into context +func Middleware(db *sql.DB) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + bearer := r.Header.Get("Authorization") + if bearer == "" { + next.ServeHTTP(w, r) + return + } + + regex, _ := regexp.Compile("^Bearer ([a-zA-Z0-9]{24})$") + matches := regex.FindStringSubmatch(bearer) + if len(matches) != 2 { + http.Error(w, "Invalid authorization header format", http.StatusBadRequest) + return + } + + token := matches[1] + log.Printf("Access token: %s\n", token) + + user, err := models.VerifyTokenAndGetUser(db, token) + if err != nil { + log.Printf("Invalid token") + http.Error(w, "Invalid authorization token", http.StatusForbidden) + return + } + + log.Printf("Found user '%s', from token\n", user.Username) + + // put it in context + ctx := context.WithValue(r.Context(), userCtxKey, user) + + // and call the next with our new context + r = r.WithContext(ctx) + next.ServeHTTP(w, r) + }) + } +} + +// UserFromContext finds the user from the context. REQUIRES Middleware to have run. +func UserFromContext(ctx context.Context) *models.User { + raw, _ := ctx.Value(userCtxKey).(*models.User) + return raw +} diff --git a/api/graphql/directive.go b/api/graphql/directive.go new file mode 100644 index 00000000..b617f6d2 --- /dev/null +++ b/api/graphql/directive.go @@ -0,0 +1,22 @@ +package api + +import ( + "context" + "database/sql" + "errors" + + "github.com/99designs/gqlgen/graphql" + "github.com/viktorstrate/photoview/api/graphql/auth" +) + +func IsAdmin(database *sql.DB) func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { + return func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { + + user := auth.UserFromContext(ctx) + if user == nil || user.Admin == false { + return nil, errors.New("user must be admin") + } + + return next(ctx) + } +} diff --git a/api/graphql/generated.go b/api/graphql/generated.go index c81ebb8f..b40bf8c4 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -6,6 +6,7 @@ import ( "bytes" "context" "errors" + "fmt" "strconv" "sync" "sync/atomic" @@ -14,6 +15,7 @@ import ( "github.com/99designs/gqlgen/graphql/introspection" "github.com/vektah/gqlparser" "github.com/vektah/gqlparser/ast" + "github.com/viktorstrate/photoview/api/graphql/models" ) // region ************************** generated!.gotpl ************************** @@ -39,6 +41,7 @@ type ResolverRoot interface { } type DirectiveRoot struct { + IsAdmin func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) } type ComplexityRoot struct { @@ -54,7 +57,8 @@ type ComplexityRoot struct { } Query struct { - Users func(childComplexity int) int + MyUser func(childComplexity int) int + Users func(childComplexity int) int } User struct { @@ -70,7 +74,8 @@ type MutationResolver interface { RegisterUser(ctx context.Context, username string, password string, rootPath string) (*AuthorizeResult, error) } type QueryResolver interface { - Users(ctx context.Context) ([]*User, error) + Users(ctx context.Context) ([]*models.User, error) + MyUser(ctx context.Context) (*models.User, error) } type executableSchema struct { @@ -133,6 +138,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.RegisterUser(childComplexity, args["username"].(string), args["password"].(string), args["rootPath"].(string)), true + case "Query.myUser": + if e.complexity.Query.MyUser == nil { + break + } + + return e.complexity.Query.MyUser(childComplexity), true + case "Query.users": if e.complexity.Query.Users == nil { break @@ -230,10 +242,14 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er } var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "graphql/schema.graphql", Input: `scalar Time + &ast.Source{Name: "graphql/schema.graphql", Input: `directive @isAdmin on FIELD_DEFINITION + +scalar Time type Query { - users: [User!]! + users: [User!]! @isAdmin + + myUser: User } type Mutation { @@ -257,7 +273,7 @@ type User { username: String! #albums: [Album] # Local filepath for the user's photos - rootPath: String! + rootPath: String! @isAdmin admin: Boolean! #shareTokens: [ShareToken] } @@ -584,8 +600,28 @@ func (ec *executionContext) _Query_users(ctx context.Context, field graphql.Coll ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Users(rctx) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Users(rctx) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAdmin == nil { + return nil, errors.New("directive isAdmin is not implemented") + } + return ec.directives.IsAdmin(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, err + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*models.User); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/viktorstrate/photoview/api/graphql/models.User`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -597,10 +633,44 @@ func (ec *executionContext) _Query_users(ctx context.Context, field graphql.Coll } return graphql.Null } - res := resTmp.([]*User) + res := resTmp.([]*models.User) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec.marshalNUser2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚐUserᚄ(ctx, field.Selections, res) + return ec.marshalNUser2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) _Query_myUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + ec.Tracer.EndFieldExecution(ctx) + }() + rctx := &graphql.ResolverContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().MyUser(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*models.User) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) } func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -678,7 +748,7 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res) } -func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *User) (ret graphql.Marshaler) { +func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -691,13 +761,13 @@ func (ec *executionContext) _User_id(ctx context.Context, field graphql.Collecte Object: "User", Field: field, Args: nil, - IsMethod: false, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.ID, nil + return obj.ID(), nil }) if err != nil { ec.Error(ctx, err) @@ -715,7 +785,7 @@ func (ec *executionContext) _User_id(ctx context.Context, field graphql.Collecte return ec.marshalNID2string(ctx, field.Selections, res) } -func (ec *executionContext) _User_username(ctx context.Context, field graphql.CollectedField, obj *User) (ret graphql.Marshaler) { +func (ec *executionContext) _User_username(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -752,7 +822,7 @@ func (ec *executionContext) _User_username(ctx context.Context, field graphql.Co return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _User_rootPath(ctx context.Context, field graphql.CollectedField, obj *User) (ret graphql.Marshaler) { +func (ec *executionContext) _User_rootPath(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -770,8 +840,28 @@ func (ec *executionContext) _User_rootPath(ctx context.Context, field graphql.Co ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.RootPath, nil + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.RootPath, nil + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAdmin == nil { + return nil, errors.New("directive isAdmin is not implemented") + } + return ec.directives.IsAdmin(ctx, obj, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, 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) @@ -789,7 +879,7 @@ func (ec *executionContext) _User_rootPath(ctx context.Context, field graphql.Co return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _User_admin(ctx context.Context, field graphql.CollectedField, obj *User) (ret graphql.Marshaler) { +func (ec *executionContext) _User_admin(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -2084,6 +2174,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) + case "myUser": + 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_myUser(ctx, field) + return res + }) case "__type": out.Values[i] = ec._Query___type(ctx, field) case "__schema": @@ -2101,7 +2202,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr var userImplementors = []string{"User"} -func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *User) graphql.Marshaler { +func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *models.User) graphql.Marshaler { fields := graphql.CollectFields(ec.RequestContext, sel, userImplementors) out := graphql.NewFieldSet(fields) @@ -2442,11 +2543,11 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S return res } -func (ec *executionContext) marshalNUser2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚐUser(ctx context.Context, sel ast.SelectionSet, v User) graphql.Marshaler { +func (ec *executionContext) marshalNUser2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v models.User) graphql.Marshaler { return ec._User(ctx, sel, &v) } -func (ec *executionContext) marshalNUser2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚐUserᚄ(ctx context.Context, sel ast.SelectionSet, v []*User) graphql.Marshaler { +func (ec *executionContext) marshalNUser2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.User) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -2470,7 +2571,7 @@ func (ec *executionContext) marshalNUser2ᚕᚖgithubᚗcomᚋviktorstrateᚋpho if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚐUser(ctx, sel, v[i]) + ret[i] = ec.marshalNUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, sel, v[i]) } if isLen1 { f(i) @@ -2483,7 +2584,7 @@ func (ec *executionContext) marshalNUser2ᚕᚖgithubᚗcomᚋviktorstrateᚋpho return ret } -func (ec *executionContext) marshalNUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚐUser(ctx context.Context, sel ast.SelectionSet, v *User) graphql.Marshaler { +func (ec *executionContext) marshalNUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v *models.User) graphql.Marshaler { if v == nil { if !ec.HasError(graphql.GetResolverContext(ctx)) { ec.Errorf(ctx, "must not be null") @@ -2765,6 +2866,17 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as return ec.marshalOString2string(ctx, sel, *v) } +func (ec *executionContext) marshalOUser2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v models.User) graphql.Marshaler { + return ec._User(ctx, sel, &v) +} + +func (ec *executionContext) marshalOUser2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v *models.User) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._User(ctx, sel, v) +} + func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go index 71b4ec70..158d95e3 100644 --- a/api/graphql/models/user.go +++ b/api/graphql/models/user.go @@ -5,6 +5,8 @@ import ( "database/sql" "errors" "fmt" + "log" + "strconv" "time" "golang.org/x/crypto/bcrypt" @@ -18,19 +20,23 @@ type User struct { Admin bool } +func (u *User) ID() string { + return strconv.Itoa(u.UserID) +} + type AccessToken struct { Value string Expire time.Time } -var UserInvalidCredentialsError = errors.New("invalid credentials") +var ErrorInvalidUserCredentials = errors.New("invalid credentials") func NewUserFromRow(row *sql.Row) (*User, error) { user := User{} if err := row.Scan(&user.UserID, &user.Username, &user.Password, &user.RootPath, &user.Admin); err != nil { if err == sql.ErrNoRows { - return nil, UserInvalidCredentialsError + return nil, ErrorInvalidUserCredentials } else { return nil, err } @@ -39,6 +45,20 @@ func NewUserFromRow(row *sql.Row) (*User, error) { return &user, nil } +func NewUsersFromRows(rows *sql.Rows) ([]*User, error) { + users := make([]*User, 0) + + for rows.Next() { + var user User + if err := rows.Scan(&user.UserID, &user.Username, &user.Password, &user.RootPath, &user.Admin); err != nil { + return nil, err + } + users = append(users, &user) + } + + return users, nil +} + func AuthorizeUser(database *sql.DB, username string, password string) (*User, error) { row := database.QueryRow("SELECT * FROM users WHERE username = ?", username) @@ -49,7 +69,7 @@ func AuthorizeUser(database *sql.DB, username string, password string) (*User, e if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { if err == bcrypt.ErrMismatchedHashAndPassword { - return nil, UserInvalidCredentialsError + return nil, ErrorInvalidUserCredentials } else { return nil, err } @@ -71,7 +91,7 @@ func RegisterUser(database *sql.DB, username string, password string, rootPath s row := database.QueryRow("SELECT * FROM users WHERE username = ?", username) if row == nil { - return nil, UserInvalidCredentialsError + return nil, ErrorInvalidUserCredentials } user, err := NewUserFromRow(row) @@ -107,3 +127,27 @@ func (user *User) GenerateAccessToken(database *sql.DB) (*AccessToken, error) { return &token, nil } + +func VerifyTokenAndGetUser(database *sql.DB, token string) (*User, error) { + + now := time.Now().UTC().Format("2006-01-02 15:04:05") + + row := database.QueryRow("SELECT (user_id) FROM access_tokens WHERE expire > ? AND value = ?", now, token) + + var userId string + + if err := row.Scan(&userId); err != nil { + log.Println(err.Error()) + return nil, err + } + + fmt.Printf("Userid: %s\n", userId) + + row = database.QueryRow("SELECT * FROM users WHERE user_id = ?", userId) + user, err := NewUserFromRow(row) + if err != nil { + return nil, err + } + + return user, nil +} diff --git a/api/graphql/models_gen.go b/api/graphql/models_gen.go index f1b3a3be..07b87773 100644 --- a/api/graphql/models_gen.go +++ b/api/graphql/models_gen.go @@ -7,10 +7,3 @@ type AuthorizeResult struct { Status string `json:"status"` Token *string `json:"token"` } - -type User struct { - ID string `json:"id"` - Username string `json:"username"` - RootPath string `json:"rootPath"` - Admin bool `json:"admin"` -} diff --git a/api/graphql/resolver.go b/api/graphql/resolver.go index 1e013ac0..9dee5ba8 100644 --- a/api/graphql/resolver.go +++ b/api/graphql/resolver.go @@ -3,6 +3,8 @@ package api import ( "context" "database/sql" + + "github.com/viktorstrate/photoview/api/graphql/models" ) //go:generate go run github.com/99designs/gqlgen @@ -14,6 +16,7 @@ type Resolver struct { func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } + func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } @@ -22,8 +25,18 @@ type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } -func (r *queryResolver) Users(ctx context.Context) ([]*User, error) { - users := make([]*User, 0) +func (r *queryResolver) Users(ctx context.Context) ([]*models.User, error) { + + rows, err := r.Database.Query("SELECT * FROM users") + if err != nil { + return nil, err + } + defer rows.Close() + + users, err := models.NewUsersFromRows(rows) + if err != nil { + return nil, err + } return users, nil } diff --git a/api/graphql/resolver_user.go b/api/graphql/resolver_user.go index a0a5e328..e80f83ed 100644 --- a/api/graphql/resolver_user.go +++ b/api/graphql/resolver_user.go @@ -3,9 +3,26 @@ package api import ( "context" + "github.com/viktorstrate/photoview/api/graphql/auth" "github.com/viktorstrate/photoview/api/graphql/models" ) +// func (r *Resolver) User() UserResolver { +// return &userResolver{r} +// } + +// type userResolver struct{ *Resolver } + +func (r *queryResolver) MyUser(ctx context.Context) (*models.User, error) { + + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return user, nil +} + func (r *mutationResolver) AuthorizeUser(ctx context.Context, username string, password string) (*AuthorizeResult, error) { user, err := models.AuthorizeUser(r.Database, username, password) if err != nil { @@ -15,7 +32,9 @@ func (r *mutationResolver) AuthorizeUser(ctx context.Context, username string, p }, nil } - token, err := user.GenerateAccessToken(r.Database) + var token *models.AccessToken + + token, err = user.GenerateAccessToken(r.Database) if err != nil { return nil, err } diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 3d24707e..c04e1d93 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -1,7 +1,11 @@ +directive @isAdmin on FIELD_DEFINITION + scalar Time type Query { - users: [User!]! + users: [User!]! @isAdmin + + myUser: User } type Mutation { @@ -25,7 +29,7 @@ type User { username: String! #albums: [Album] # Local filepath for the user's photos - rootPath: String! + rootPath: String! @isAdmin admin: Boolean! #shareTokens: [ShareToken] } diff --git a/api/server/server.go b/api/server/server.go index b5fe12b3..b72c3d93 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -5,9 +5,11 @@ import ( "net/http" "os" + "github.com/go-chi/chi" "github.com/joho/godotenv" "github.com/viktorstrate/photoview/api/database" + "github.com/viktorstrate/photoview/api/graphql/auth" "github.com/99designs/gqlgen/handler" photoview_graphql "github.com/viktorstrate/photoview/api/graphql" @@ -34,11 +36,21 @@ func main() { log.Fatalf("Could not migrate database: %s\n", err) } - graphqlResolver := photoview_graphql.Resolver{Database: db} + router := chi.NewRouter() + router.Use(auth.Middleware(db)) - http.Handle("/", handler.Playground("GraphQL playground", "/query")) - http.Handle("/query", handler.GraphQL(photoview_graphql.NewExecutableSchema(photoview_graphql.Config{Resolvers: &graphqlResolver}))) + graphqlResolver := photoview_graphql.Resolver{Database: db} + graphqlDirective := photoview_graphql.DirectiveRoot{} + graphqlDirective.IsAdmin = photoview_graphql.IsAdmin(db) + + graphqlConfig := photoview_graphql.Config{ + Resolvers: &graphqlResolver, + Directives: graphqlDirective, + } + + router.Handle("/", handler.Playground("GraphQL playground", "/query")) + router.Handle("/query", handler.GraphQL(photoview_graphql.NewExecutableSchema(graphqlConfig))) log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) - log.Fatal(http.ListenAndServe(":"+port, nil)) + log.Fatal(http.ListenAndServe(":"+port, router)) }