Initial Commit

This commit is contained in:
2021-02-11 15:47:42 -05:00
commit fec590b16e
249 changed files with 42571 additions and 0 deletions

27
internal/api/api.go Normal file
View File

@@ -0,0 +1,27 @@
package api
import (
"net/http"
"reichard.io/imagini/internal/db"
"reichard.io/imagini/internal/auth"
"reichard.io/imagini/internal/config"
)
type API struct {
Router *http.ServeMux
Config *config.Config
Auth *auth.AuthManager
DB *db.DBManager
}
func NewApi(db *db.DBManager, c *config.Config, auth *auth.AuthManager) *API {
api := &API{
Router: http.NewServeMux(),
Config: c,
Auth: auth,
DB: db,
}
api.registerRoutes()
return api
}

103
internal/api/auth.go Normal file
View File

@@ -0,0 +1,103 @@
package api
import (
"errors"
"fmt"
"net/http"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/jwt"
"reichard.io/imagini/graph/model"
)
func (api *API) refreshTokens(refreshToken jwt.Token) (string, string, error) {
// Acquire User & Device
did, ok := refreshToken.Get("did")
if !ok {
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")
}
// Device & User Skeleton
user := model.User{ID: userUUID.String()}
device := model.Device{ID: deviceUUID.String()}
// Find User
_, err = api.DB.User(&user)
if err != nil {
return "", "", err
}
// Update Access Token
accessTokenCookie, err := api.Auth.CreateJWTAccessToken(user, device)
if err != nil {
return "", "", err
}
return accessTokenCookie, "", err
}
func (api *API) validateTokens(w *http.ResponseWriter, r *http.Request) (jwt.Token, error) {
// TODO: Check from X-Imagini-AccessToken
// TODO: Check from X-Imagini-RefreshToken
// Validate Access Token
accessCookie, _ := r.Cookie("AccessToken")
if accessCookie != nil {
accessToken, err := api.Auth.ValidateJWTAccessToken(accessCookie.Value)
if err == nil {
return accessToken, nil
}
}
// Validate Refresh Cookie Exists
refreshCookie, _ := r.Cookie("RefreshToken")
if refreshCookie == nil {
return nil, errors.New("Tokens Invalid")
}
// Validate Refresh Token
refreshToken, err := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value)
if err != nil {
return nil, errors.New("Tokens Invalid")
}
// Refresh Access Token & Generate New Refresh Token
newAccessToken, newRefreshToken, err := api.refreshTokens(refreshToken)
if err != nil {
return nil, err
}
// TODO: Actually Refresh Refresh Token
newRefreshToken = refreshCookie.Value
// Set appropriate cookies (TODO: Only for web!)
// Update Access & Refresh Cookies
http.SetCookie(*w, &http.Cookie{
Name: "AccessToken",
Value: newAccessToken,
})
http.SetCookie(*w, &http.Cookie{
Name: "RefreshToken",
Value: newRefreshToken,
})
// Only for iOS & Android (TODO: Remove for web! Only cause affected by CORS during development)
(*w).Header().Set("X-Imagini-AccessToken", newAccessToken)
(*w).Header().Set("X-Imagini-RefreshToken", newRefreshToken)
return jwt.ParseBytes([]byte(newAccessToken))
}

View 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")
}

55
internal/api/media.go Normal file
View File

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

View File

@@ -0,0 +1 @@
package api

View File

@@ -0,0 +1,71 @@
package api
import (
"context"
"net/http"
"reichard.io/imagini/graph/model"
)
type Middleware func(http.Handler) http.HandlerFunc
func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc {
if len(m) < 1 {
return h
}
wrapped := h
for i := len(m) - 1; i >= 0; i-- {
wrapped = m[i](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) queryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: REMOVE (SOME OF) THIS!! Only for developement due to CORS
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Expose-Headers", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
authContext := &model.AuthContext{
AuthResponse: &w,
AuthRequest: r,
}
// Add context
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 {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate Tokens
accessToken, err := api.validateTokens(&w, r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
// Create Context
authContext := &model.AuthContext{
AccessToken: &accessToken,
}
ctx := context.WithValue(r.Context(), "auth", authContext)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}

36
internal/api/routes.go Normal file
View File

@@ -0,0 +1,36 @@
package api
import (
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"reichard.io/imagini/graph"
"reichard.io/imagini/graph/generated"
)
func (api *API) registerRoutes() {
// Set up Directives
graphConfig := generated.Config{
Resolvers: &graph.Resolver{
DB: api.DB,
Auth: api.Auth,
Config: api.Config,
},
Directives: generated.DirectiveRoot{
Meta: api.metaDirective,
IsPrivate: api.isPrivateDirective,
HasMinRole: api.hasMinRoleDirective,
},
}
srv := handler.NewDefaultServer(generated.NewExecutableSchema(graphConfig))
// Handle GraphQL
api.Router.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
api.Router.Handle("/query", api.queryMiddleware(srv))
// Handle Resource Route
api.Router.HandleFunc("/media/", multipleMiddleware(
api.mediaHandler,
api.authMiddleware,
))
}