Enable GraphQL! #1
@ -1,15 +1,16 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"reichard.io/imagini/internal/db"
|
|
||||||
"reichard.io/imagini/internal/api"
|
"reichard.io/imagini/internal/api"
|
||||||
"reichard.io/imagini/internal/auth"
|
"reichard.io/imagini/internal/auth"
|
||||||
"reichard.io/imagini/internal/config"
|
"reichard.io/imagini/internal/config"
|
||||||
|
"reichard.io/imagini/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
|
@ -43,6 +43,7 @@ type ResolverRoot interface {
|
|||||||
|
|
||||||
type DirectiveRoot struct {
|
type DirectiveRoot struct {
|
||||||
HasMinRole func(ctx context.Context, obj interface{}, next graphql.Resolver, role model.Role) (res interface{}, err error)
|
HasMinRole func(ctx context.Context, obj interface{}, next graphql.Resolver, role model.Role) (res interface{}, err error)
|
||||||
|
IsPrivate func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error)
|
||||||
Meta func(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string) (res interface{}, err error)
|
Meta func(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string) (res interface{}, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,21 +244,21 @@ 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":
|
case "AuthResponse.device":
|
||||||
if e.complexity.AuthResponse.Device == nil {
|
if e.complexity.AuthResponse.Device == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.complexity.AuthResponse.Device(childComplexity), true
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.complexity.AuthResponse.Error(childComplexity), true
|
return e.complexity.AuthResponse.Error(childComplexity), true
|
||||||
|
|
||||||
case "AuthResponse.Result":
|
case "AuthResponse.result":
|
||||||
if e.complexity.AuthResponse.Result == nil {
|
if e.complexity.AuthResponse.Result == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -863,6 +864,7 @@ scalar Upload
|
|||||||
|
|
||||||
# https://gqlgen.com/reference/directives/
|
# https://gqlgen.com/reference/directives/
|
||||||
directive @hasMinRole(role: Role!) on FIELD_DEFINITION
|
directive @hasMinRole(role: Role!) on FIELD_DEFINITION
|
||||||
|
directive @isPrivate on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
|
||||||
|
|
||||||
directive @meta(
|
directive @meta(
|
||||||
gorm: String,
|
gorm: String,
|
||||||
@ -899,9 +901,9 @@ enum AuthResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthResponse {
|
type AuthResponse {
|
||||||
Result: AuthResult!
|
result: AuthResult!
|
||||||
Device: Device
|
device: Device
|
||||||
Error: String
|
error: String
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@ -1004,7 +1006,7 @@ type Device {
|
|||||||
type: DeviceType! @meta(gorm: "default:Unknown;not null")
|
type: DeviceType! @meta(gorm: "default:Unknown;not null")
|
||||||
userID: ID! @meta(gorm: "not null")
|
userID: ID! @meta(gorm: "not null")
|
||||||
user: User! @meta(gorm: "foreignKey:ID;references:UserID;not null")
|
user: User! @meta(gorm: "foreignKey:ID;references:UserID;not null")
|
||||||
refreshKey: String
|
refreshKey: String @deprecated(reason: "Private Field") # @isPrivate
|
||||||
}
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
@ -1017,7 +1019,7 @@ type User {
|
|||||||
lastName: String
|
lastName: String
|
||||||
role: Role! @meta(gorm: "default:User;not null")
|
role: Role! @meta(gorm: "default:User;not null")
|
||||||
authType: AuthType! @meta(gorm: "default:Local;not null")
|
authType: AuthType! @meta(gorm: "default:Local;not null")
|
||||||
password: String
|
password: String @deprecated(reason: "Private Field") #@isPrivate
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaItem {
|
type MediaItem {
|
||||||
@ -1927,7 +1929,7 @@ func (ec *executionContext) _AlbumResponse_pageInfo(ctx context.Context, field g
|
|||||||
return ec.marshalNPageInfo2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐPageInfo(ctx, field.Selections, res)
|
return ec.marshalNPageInfo2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐPageInfo(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _AuthResponse_Result(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
|
func (ec *executionContext) _AuthResponse_result(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 {
|
||||||
ec.Error(ctx, ec.Recover(ctx, r))
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
@ -1962,7 +1964,7 @@ 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) {
|
func (ec *executionContext) _AuthResponse_device(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 {
|
||||||
ec.Error(ctx, ec.Recover(ctx, r))
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
@ -1994,7 +1996,7 @@ func (ec *executionContext) _AuthResponse_Device(ctx context.Context, field grap
|
|||||||
return ec.marshalODevice2ᚖreichardᚗioᚋimaginiᚋgraphᚋmodelᚐDevice(ctx, field.Selections, 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 {
|
||||||
ec.Error(ctx, ec.Recover(ctx, r))
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
@ -7553,15 +7555,15 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection
|
|||||||
switch field.Name {
|
switch field.Name {
|
||||||
case "__typename":
|
case "__typename":
|
||||||
out.Values[i] = graphql.MarshalString("AuthResponse")
|
out.Values[i] = graphql.MarshalString("AuthResponse")
|
||||||
case "Result":
|
case "result":
|
||||||
out.Values[i] = ec._AuthResponse_Result(ctx, field, obj)
|
out.Values[i] = ec._AuthResponse_result(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
invalids++
|
invalids++
|
||||||
}
|
}
|
||||||
case "Device":
|
case "device":
|
||||||
out.Values[i] = ec._AuthResponse_Device(ctx, field, obj)
|
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:
|
||||||
panic("unknown field " + strconv.Quote(field.Name))
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,9 @@ type AlbumResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthResponse struct {
|
type AuthResponse struct {
|
||||||
Result AuthResult `json:"Result" `
|
Result AuthResult `json:"result" `
|
||||||
Device *Device `json:"Device" `
|
Device *Device `json:"device" `
|
||||||
Error *string `json:"Error" `
|
Error *string `json:"error" `
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthTypeFilter struct {
|
type AuthTypeFilter struct {
|
||||||
|
@ -5,6 +5,7 @@ scalar Upload
|
|||||||
|
|
||||||
# https://gqlgen.com/reference/directives/
|
# https://gqlgen.com/reference/directives/
|
||||||
directive @hasMinRole(role: Role!) on FIELD_DEFINITION
|
directive @hasMinRole(role: Role!) on FIELD_DEFINITION
|
||||||
|
directive @isPrivate on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
|
||||||
|
|
||||||
directive @meta(
|
directive @meta(
|
||||||
gorm: String,
|
gorm: String,
|
||||||
@ -41,9 +42,9 @@ enum AuthResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthResponse {
|
type AuthResponse {
|
||||||
Result: AuthResult!
|
result: AuthResult!
|
||||||
Device: Device
|
device: Device
|
||||||
Error: String
|
error: String
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@ -146,7 +147,7 @@ type Device {
|
|||||||
type: DeviceType! @meta(gorm: "default:Unknown;not null")
|
type: DeviceType! @meta(gorm: "default:Unknown;not null")
|
||||||
userID: ID! @meta(gorm: "not null")
|
userID: ID! @meta(gorm: "not null")
|
||||||
user: User! @meta(gorm: "foreignKey:ID;references:UserID;not null")
|
user: User! @meta(gorm: "foreignKey:ID;references:UserID;not null")
|
||||||
refreshKey: String
|
refreshKey: String @isPrivate
|
||||||
}
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
@ -159,7 +160,7 @@ type User {
|
|||||||
lastName: String
|
lastName: String
|
||||||
role: Role! @meta(gorm: "default:User;not null")
|
role: Role! @meta(gorm: "default:User;not null")
|
||||||
authType: AuthType! @meta(gorm: "default:Local;not null")
|
authType: AuthType! @meta(gorm: "default:Local;not null")
|
||||||
password: String
|
password: String @isPrivate
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaItem {
|
type MediaItem {
|
||||||
|
@ -75,6 +75,8 @@ func (r *queryResolver) Login(ctx context.Context, user string, password string,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
foundDevice.Type = deriveDeviceType(req)
|
foundDevice.Type = deriveDeviceType(req)
|
||||||
|
foundDevice.UserID = foundUser.ID
|
||||||
|
// TODO: foundDevice.User = &foundUser
|
||||||
err := r.DB.CreateDevice(&foundDevice)
|
err := r.DB.CreateDevice(&foundDevice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
|
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
|
||||||
@ -97,6 +99,8 @@ func (r *queryResolver) Login(ctx context.Context, user string, password string,
|
|||||||
http.SetCookie(*resp, &accessCookie)
|
http.SetCookie(*resp, &accessCookie)
|
||||||
http.SetCookie(*resp, &refreshCookie)
|
http.SetCookie(*resp, &refreshCookie)
|
||||||
|
|
||||||
|
// TODO: Prob bandaid
|
||||||
|
foundDevice.User = &foundUser
|
||||||
return &model.AuthResponse{Result: model.AuthResultSuccess, Device: &foundDevice}, nil
|
return &model.AuthResponse{Result: model.AuthResultSuccess, Device: &foundDevice}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/lestrrat-go/jwx/jwt"
|
"github.com/lestrrat-go/jwx/jwt"
|
||||||
|
|
||||||
@ -94,31 +92,3 @@ func (api *API) validateTokens(w *http.ResponseWriter, r *http.Request) (jwt.Tok
|
|||||||
|
|
||||||
return jwt.ParseBytes([]byte(newAccessCookie))
|
return jwt.ParseBytes([]byte(newAccessCookie))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) hasMinRoleDirective(ctx context.Context, obj interface{}, next graphql.Resolver, role model.Role) (res interface{}, err error) {
|
|
||||||
authContext := ctx.Value("auth").(*model.AuthContext)
|
|
||||||
accessToken, err := api.validateTokens(authContext.AuthResponse, authContext.AuthRequest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Access Denied")
|
|
||||||
}
|
|
||||||
authContext.AccessToken = &accessToken
|
|
||||||
|
|
||||||
userRole, ok := accessToken.Get("role")
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("Access Denied")
|
|
||||||
}
|
|
||||||
|
|
||||||
if userRole == model.RoleAdmin.String() {
|
|
||||||
return next(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
if userRole == role.String() {
|
|
||||||
return next(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("Role Not Authenticated")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) metaDirective(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string) (res interface{}, err error) {
|
|
||||||
return next(ctx)
|
|
||||||
}
|
|
||||||
|
50
internal/api/directives.go
Normal file
50
internal/api/directives.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql"
|
||||||
|
"reichard.io/imagini/graph/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used to validate whether the users role is adequate for the requested resource.
|
||||||
|
**/
|
||||||
|
func (api *API) hasMinRoleDirective(ctx context.Context, obj interface{}, next graphql.Resolver, role model.Role) (res interface{}, err error) {
|
||||||
|
authContext := ctx.Value("auth").(*model.AuthContext)
|
||||||
|
accessToken, err := api.validateTokens(authContext.AuthResponse, authContext.AuthRequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Access Denied")
|
||||||
|
}
|
||||||
|
authContext.AccessToken = &accessToken
|
||||||
|
|
||||||
|
userRole, ok := accessToken.Get("role")
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("Access Denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
if userRole == model.RoleAdmin.String() {
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userRole == role.String() {
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("Role Not Authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is needed but not used. Meta is used for Gorm.
|
||||||
|
**/
|
||||||
|
func (api *API) metaDirective(ctx context.Context, obj interface{}, next graphql.Resolver, gorm *string) (res interface{}, err error) {
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This overrides the response so fields with an @isPrivate directive are always nil.
|
||||||
|
**/
|
||||||
|
func (api *API) isPrivateDirective(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
|
||||||
|
return nil, errors.New("Private Field")
|
||||||
|
}
|
@ -8,7 +8,9 @@ import (
|
|||||||
"reichard.io/imagini/graph/model"
|
"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)
|
||||||
|
@ -3,9 +3,6 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"reichard.io/imagini/graph/model"
|
"reichard.io/imagini/graph/model"
|
||||||
)
|
)
|
||||||
@ -27,9 +24,8 @@ func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc {
|
|||||||
* This is used for the graphQL endpoints that may require access to the
|
* This is used for the graphQL endpoints that may require access to the
|
||||||
* Request and ResponseWriter variables. These are used to get / set cookies.
|
* Request and ResponseWriter variables. These are used to get / set cookies.
|
||||||
**/
|
**/
|
||||||
func (api *API) injectContextMiddleware(next http.Handler) http.Handler {
|
func (api *API) contextMiddleware(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) {
|
||||||
|
|
||||||
authContext := &model.AuthContext{
|
authContext := &model.AuthContext{
|
||||||
AuthResponse: &w,
|
AuthResponse: &w,
|
||||||
AuthRequest: r,
|
AuthRequest: r,
|
||||||
@ -40,7 +36,6 @@ func (api *API) injectContextMiddleware(next http.Handler) http.Handler {
|
|||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,10 +44,10 @@ func (api *API) injectContextMiddleware(next http.Handler) http.Handler {
|
|||||||
**/
|
**/
|
||||||
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) {
|
||||||
|
// Validate Tokens
|
||||||
accessToken, err := api.validateTokens(&w, r)
|
accessToken, err := api.validateTokens(&w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,16 +59,5 @@ func (api *API) authMiddleware(next http.Handler) http.HandlerFunc {
|
|||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) logMiddleware(h http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
|
|
||||||
log.SetOutput(os.Stdout)
|
|
||||||
log.Println(r.Method, r.URL)
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
type APICredentials struct {
|
|
||||||
User string `json:"user"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type APIData interface{}
|
|
||||||
|
|
||||||
type APIMeta struct {
|
|
||||||
Count int64 `json:"count"`
|
|
||||||
Page int64 `json:"page"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type APIError struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Code int64 `json:"code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type APIResponse struct {
|
|
||||||
Data APIData `json:"data,omitempty"`
|
|
||||||
Meta *APIMeta `json:"meta,omitempty"`
|
|
||||||
Error *APIError `json:"error,omitempty"`
|
|
||||||
}
|
|
@ -1,9 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"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"
|
||||||
|
|
||||||
@ -12,7 +9,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (api *API) registerRoutes() {
|
func (api *API) registerRoutes() {
|
||||||
|
|
||||||
// Set up Directives
|
// Set up Directives
|
||||||
graphConfig := generated.Config{
|
graphConfig := generated.Config{
|
||||||
Resolvers: &graph.Resolver{
|
Resolvers: &graph.Resolver{
|
||||||
@ -21,6 +17,7 @@ func (api *API) registerRoutes() {
|
|||||||
},
|
},
|
||||||
Directives: generated.DirectiveRoot{
|
Directives: generated.DirectiveRoot{
|
||||||
Meta: api.metaDirective,
|
Meta: api.metaDirective,
|
||||||
|
IsPrivate: api.isPrivateDirective,
|
||||||
HasMinRole: api.hasMinRoleDirective,
|
HasMinRole: api.hasMinRoleDirective,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -28,31 +25,11 @@ func (api *API) registerRoutes() {
|
|||||||
|
|
||||||
// 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.contextMiddleware(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) {
|
|
||||||
errStruct := &APIResponse{Error: &APIError{Message: err, Code: int64(code)}}
|
|
||||||
responseJSON(w, errStruct, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func responseJSON(w http.ResponseWriter, msg interface{}, code int) {
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
||||||
w.WriteHeader(code)
|
|
||||||
json.NewEncoder(w).Encode(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func successJSON(w http.ResponseWriter, msg string, code int) {
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
||||||
w.WriteHeader(code)
|
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{"success": msg})
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/lestrrat-go/jwx/jwa"
|
|
||||||
"github.com/lestrrat-go/jwx/jwt"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
@ -61,115 +55,3 @@ func (auth *AuthManager) AuthenticateUser(user, password string) (model.User, bo
|
|||||||
return *foundUser, false
|
return *foundUser, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, error) {
|
|
||||||
byteRefreshJWT := []byte(refreshJWT)
|
|
||||||
|
|
||||||
// Acquire Relevant Device
|
|
||||||
unverifiedToken, err := jwt.ParseBytes(byteRefreshJWT)
|
|
||||||
did, ok := unverifiedToken.Get("did")
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("did does not exist")
|
|
||||||
}
|
|
||||||
deviceID, err := uuid.Parse(fmt.Sprintf("%v", did))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("did does not parse")
|
|
||||||
}
|
|
||||||
device := &model.Device{ID: deviceID.String()}
|
|
||||||
_, err = auth.DB.Device(device)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify & Validate Token
|
|
||||||
verifiedToken, err := jwt.ParseBytes(byteRefreshJWT,
|
|
||||||
jwt.WithValidate(true),
|
|
||||||
jwt.WithVerify(jwa.HS256, []byte(*device.RefreshKey)),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("failed to parse payload: ", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return verifiedToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *AuthManager) ValidateJWTAccessToken(accessJWT string) (jwt.Token, error) {
|
|
||||||
byteAccessJWT := []byte(accessJWT)
|
|
||||||
verifiedToken, err := jwt.ParseBytes(byteAccessJWT,
|
|
||||||
jwt.WithValidate(true),
|
|
||||||
jwt.WithVerify(jwa.HS256, []byte(auth.Config.JWTSecret)),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return verifiedToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *AuthManager) CreateJWTRefreshToken(user model.User, device model.Device) (string, error) {
|
|
||||||
// Acquire Refresh Key
|
|
||||||
byteKey := []byte(*device.RefreshKey)
|
|
||||||
|
|
||||||
// Create New Token
|
|
||||||
tm := time.Now()
|
|
||||||
t := jwt.New()
|
|
||||||
t.Set(`did`, device.ID) // Device ID
|
|
||||||
t.Set(jwt.SubjectKey, user.ID) // User ID
|
|
||||||
t.Set(jwt.AudienceKey, `imagini`) // App ID
|
|
||||||
t.Set(jwt.IssuedAtKey, tm) // Issued At
|
|
||||||
|
|
||||||
// iOS & Android = Never Expiring Refresh Token
|
|
||||||
if device.Type != "iOS" && device.Type != "Android" {
|
|
||||||
t.Set(jwt.ExpirationKey, tm.Add(time.Hour*24)) // 1 Day Access Key
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate Token Creation
|
|
||||||
_, err := json.MarshalIndent(t, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to generate JSON: %s\n", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign Token
|
|
||||||
signed, err := jwt.Sign(t, jwa.HS256, byteKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to sign token: %s", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return Token
|
|
||||||
return string(signed), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *AuthManager) CreateJWTAccessToken(user model.User, device model.Device) (string, error) {
|
|
||||||
// Create New Token
|
|
||||||
tm := time.Now()
|
|
||||||
t := jwt.New()
|
|
||||||
t.Set(`did`, device.ID) // Device ID
|
|
||||||
t.Set(`role`, user.Role.String()) // User Role (Admin / User)
|
|
||||||
t.Set(jwt.SubjectKey, user.ID) // User ID
|
|
||||||
t.Set(jwt.AudienceKey, `imagini`) // App ID
|
|
||||||
t.Set(jwt.IssuedAtKey, tm) // Issued At
|
|
||||||
t.Set(jwt.ExpirationKey, tm.Add(time.Hour*2)) // 2 Hour Access Key
|
|
||||||
|
|
||||||
// Validate Token Creation
|
|
||||||
_, err := json.MarshalIndent(t, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to generate JSON: %s\n", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Server Key
|
|
||||||
byteKey := []byte(auth.Config.JWTSecret)
|
|
||||||
|
|
||||||
// Sign Token
|
|
||||||
signed, err := jwt.Sign(t, jwa.HS256, byteKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to sign token: %s", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return Token
|
|
||||||
return string(signed), nil
|
|
||||||
}
|
|
||||||
|
126
internal/auth/jwt.go
Normal file
126
internal/auth/jwt.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/lestrrat-go/jwx/jwa"
|
||||||
|
"github.com/lestrrat-go/jwx/jwt"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"reichard.io/imagini/graph/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, error) {
|
||||||
|
byteRefreshJWT := []byte(refreshJWT)
|
||||||
|
|
||||||
|
// Acquire Relevant Device
|
||||||
|
unverifiedToken, err := jwt.ParseBytes(byteRefreshJWT)
|
||||||
|
did, ok := unverifiedToken.Get("did")
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("did does not exist")
|
||||||
|
}
|
||||||
|
deviceID, err := uuid.Parse(fmt.Sprintf("%v", did))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("did does not parse")
|
||||||
|
}
|
||||||
|
device := &model.Device{ID: deviceID.String()}
|
||||||
|
_, err = auth.DB.Device(device)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify & Validate Token
|
||||||
|
verifiedToken, err := jwt.ParseBytes(byteRefreshJWT,
|
||||||
|
jwt.WithValidate(true),
|
||||||
|
jwt.WithVerify(jwa.HS256, []byte(*device.RefreshKey)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("failed to parse payload: ", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return verifiedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthManager) ValidateJWTAccessToken(accessJWT string) (jwt.Token, error) {
|
||||||
|
byteAccessJWT := []byte(accessJWT)
|
||||||
|
verifiedToken, err := jwt.ParseBytes(byteAccessJWT,
|
||||||
|
jwt.WithValidate(true),
|
||||||
|
jwt.WithVerify(jwa.HS256, []byte(auth.Config.JWTSecret)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifiedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthManager) CreateJWTRefreshToken(user model.User, device model.Device) (string, error) {
|
||||||
|
// Acquire Refresh Key
|
||||||
|
byteKey := []byte(*device.RefreshKey)
|
||||||
|
|
||||||
|
// Create New Token
|
||||||
|
tm := time.Now()
|
||||||
|
t := jwt.New()
|
||||||
|
t.Set(`did`, device.ID) // Device ID
|
||||||
|
t.Set(jwt.SubjectKey, user.ID) // User ID
|
||||||
|
t.Set(jwt.AudienceKey, `imagini`) // App ID
|
||||||
|
t.Set(jwt.IssuedAtKey, tm) // Issued At
|
||||||
|
|
||||||
|
// iOS & Android = Never Expiring Refresh Token
|
||||||
|
if device.Type != "iOS" && device.Type != "Android" {
|
||||||
|
t.Set(jwt.ExpirationKey, tm.Add(time.Hour*24)) // 1 Day Access Key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Token Creation
|
||||||
|
_, err := json.MarshalIndent(t, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to generate JSON: %s\n", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign Token
|
||||||
|
signed, err := jwt.Sign(t, jwa.HS256, byteKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to sign token: %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return Token
|
||||||
|
return string(signed), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *AuthManager) CreateJWTAccessToken(user model.User, device model.Device) (string, error) {
|
||||||
|
// Create New Token
|
||||||
|
tm := time.Now()
|
||||||
|
t := jwt.New()
|
||||||
|
t.Set(`did`, device.ID) // Device ID
|
||||||
|
t.Set(`role`, user.Role.String()) // User Role (Admin / User)
|
||||||
|
t.Set(jwt.SubjectKey, user.ID) // User ID
|
||||||
|
t.Set(jwt.AudienceKey, `imagini`) // App ID
|
||||||
|
t.Set(jwt.IssuedAtKey, tm) // Issued At
|
||||||
|
t.Set(jwt.ExpirationKey, tm.Add(time.Hour*2)) // 2 Hour Access Key
|
||||||
|
|
||||||
|
// Validate Token Creation
|
||||||
|
_, err := json.MarshalIndent(t, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to generate JSON: %s\n", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Server Key
|
||||||
|
byteKey := []byte(auth.Config.JWTSecret)
|
||||||
|
|
||||||
|
// Sign Token
|
||||||
|
signed, err := jwt.Sign(t, jwa.HS256, byteKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to sign token: %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return Token
|
||||||
|
return string(signed), nil
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -69,6 +69,7 @@ func (dbm *DBManager) bootstrapDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
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
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrUserAlreadyExists = errors.New("user already exists")
|
|
||||||
)
|
|
@ -6,6 +6,8 @@ import (
|
|||||||
|
|
||||||
// Used to maintain a cache of user specific jwt secrets
|
// Used to maintain a cache of user specific jwt secrets
|
||||||
// This will prevent DB lookups on every request
|
// This will prevent DB lookups on every request
|
||||||
|
// May not actually be needed. Refresh Token is the only
|
||||||
|
// token that will require proactive DB lookups.
|
||||||
type SessionManager struct {
|
type SessionManager struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
values map[string]string
|
values map[string]string
|
||||||
|
Reference in New Issue
Block a user