Wooo API!

This commit is contained in:
2021-02-04 05:16:13 -05:00
parent c39fe6ec24
commit 082f923482
18 changed files with 977 additions and 795 deletions

View File

@@ -1,248 +1,124 @@
package api
import (
"fmt"
"time"
"strings"
"context"
"net/http"
"encoding/json"
"context"
"errors"
"fmt"
"net/http"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/lestrrat-go/jwx/jwt"
"github.com/99designs/gqlgen/graphql"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/jwt"
"reichard.io/imagini/graph/model"
)
func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
if r.Method != http.MethodPost {
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
return
}
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")
}
// Decode into Struct
var creds APICredentials
err := json.NewDecoder(r.Body).Decode(&creds)
if err != nil {
errorJSON(w, "Invalid parameters.", http.StatusBadRequest)
return
}
// Device & User Skeleton
user := model.User{ID: userUUID.String()}
device := model.Device{ID: deviceUUID.String()}
// Validate
if creds.User == "" || creds.Password == "" {
errorJSON(w, "Invalid parameters.", http.StatusBadRequest)
return
}
// Find User
_, err = api.DB.User(&user)
if err != nil {
return "", "", err
}
// Do login
resp, user := api.Auth.AuthenticateUser(creds.User, creds.Password)
if !resp {
errorJSON(w, "Invalid credentials.", http.StatusUnauthorized)
return
}
// Update Access Token
accessTokenCookie, err := api.Auth.CreateJWTAccessToken(user, device)
if err != nil {
return "", "", err
}
// Upsert device
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)
return accessTokenCookie, "", err
}
func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed)
return
}
func (api *API) validateTokens(w *http.ResponseWriter, r *http.Request) (jwt.Token, error) {
// Validate Access Token
accessCookie, _ := r.Cookie("AccessToken")
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
http.SetCookie(w, &http.Cookie{Name: "AccessToken", Expires: time.Unix(0, 0)})
http.SetCookie(w, &http.Cookie{Name: "RefreshToken", Expires: time.Unix(0, 0)})
// Validate Refresh Token
refreshToken, err := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value)
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))
}
/**
* This will find or create the requested device based on ID and User.
**/
func (api *API) upsertRequestedDevice(user model.User, r *http.Request) (model.Device, error) {
requestedDevice := deriveRequestedDevice(r)
requestedDevice.Type = deriveDeviceType(r)
requestedDevice.User.ID = user.ID
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
if *requestedDevice.ID == "" {
err := api.DB.CreateDevice(&requestedDevice)
createdDevice, err := api.DB.Device(&requestedDevice)
return createdDevice, err
}
userRole, ok := accessToken.Get("role")
if !ok {
return nil, errors.New("Access Denied")
}
foundDevice, err := api.DB.Device(&model.Device{
ID: requestedDevice.ID,
User: &user,
})
if userRole == model.RoleAdmin.String() {
return next(ctx)
}
return foundDevice, err
if userRole == role.String() {
return next(ctx)
}
return nil, errors.New("Role Not Authenticated")
}
func deriveDeviceType(r *http.Request) model.DeviceType {
userAgent := strings.ToLower(r.Header.Get("User-Agent"))
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)
func (api *API) 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
import (
"os"
"path"
"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 r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if path.Dir(r.URL.Path) != "/media" {
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
// 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
// TODO: Caching & Resizing
// - If both, force resize with new scale
// - If one, scale resize proportionally
// Pull out UUIDs
reqInfo := r.Context().Value("uuids").(map[string]string)
uid := reqInfo["uid"]
// 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/" + uid)
mediaPath := path.Join(folderPath + "/" + fileName)
// 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
}
// 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)
http.ServeFile(w, r, mediaPath)
}

View File

@@ -1,104 +1,79 @@
package api
import (
log "github.com/sirupsen/logrus"
"net/http"
"context"
"os"
"context"
"net/http"
"os"
"reichard.io/imagini/graph/model"
log "github.com/sirupsen/logrus"
"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
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) injectContextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Info("[middleware] Entering testMiddleware...")
authContext := &model.AuthContext{
AuthResponse: &w,
AuthRequest: r,
}
accessCookie, err := r.Cookie("AccessToken")
if err != nil {
log.Warn("[middleware] AccessToken not found")
} else {
authContext.AccessToken = accessCookie.Value
}
refreshCookie, err := r.Cookie("RefreshToken")
if err != nil {
log.Warn("[middleware] RefreshToken not found")
} else {
authContext.RefreshToken = refreshCookie.Value
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Add context
ctx := context.WithValue(r.Context(), "auth", authContext)
r = r.WithContext(ctx)
authContext := &model.AuthContext{
AuthResponse: &w,
AuthRequest: r,
}
log.Info("[middleware] Exiting testMiddleware...")
next.ServeHTTP(w, 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) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Acquire Token
accessCookie, err := r.Cookie("AccessToken")
if err != nil {
log.Warn("[middleware] AccessToken not found")
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
return
}
accessToken, err := api.validateTokens(&w, r)
if err != nil {
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
return
}
// Validate JWT Tokens
accessToken, err := api.Auth.ValidateJWTAccessToken(accessCookie.Value)
// Create Context
authContext := &model.AuthContext{
AccessToken: &accessToken,
}
ctx := context.WithValue(r.Context(), "auth", authContext)
r = r.WithContext(ctx)
if err != nil && err.Error() == "exp not satisfied" {
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
}
next.ServeHTTP(w, r)
// 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 {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.SetOutput(os.Stdout)
log.Println(r.Method, r.URL)
h.ServeHTTP(w, r)
})
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.SetOutput(os.Stdout)
log.Println(r.Method, r.URL)
h.ServeHTTP(w, r)
})
}

View File

@@ -1,8 +1,8 @@
package api
import (
"net/http"
"encoding/json"
"encoding/json"
"net/http"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
@@ -13,39 +13,46 @@ import (
func (api *API) registerRoutes() {
// Set up Directives
c := generated.Config{ Resolvers: &graph.Resolver{ DB: api.DB } }
c.Directives.HasMinRole = hasMinRoleDirective
c.Directives.Meta = metaDirective
srv := handler.NewDefaultServer(generated.NewExecutableSchema(c))
// Set up Directives
graphConfig := generated.Config{
Resolvers: &graph.Resolver{
DB: api.DB,
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("/query", api.injectContextMiddleware(srv))
// Handle Resource Route
api.Router.HandleFunc("/media/", multipleMiddleware(
api.mediaHandler,
api.authMiddleware,
))
// Handle Resource Route
api.Router.HandleFunc("/media/", multipleMiddleware(
api.mediaHandler,
api.authMiddleware,
))
}
func errorJSON(w http.ResponseWriter, err string, code int) {
errStruct := &APIResponse{Error: &APIError{Message: err, Code: int64(code)}}
responseJSON(w, errStruct, code)
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)
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})
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})
}

View File

@@ -1,185 +1,175 @@
package auth
import (
"fmt"
"time"
"errors"
"encoding/json"
"encoding/json"
"errors"
"fmt"
"time"
"gorm.io/gorm"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwt"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwt"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"reichard.io/imagini/graph/model"
"reichard.io/imagini/internal/db"
"reichard.io/imagini/internal/config"
"reichard.io/imagini/internal/session"
"reichard.io/imagini/graph/model"
"reichard.io/imagini/internal/config"
"reichard.io/imagini/internal/db"
)
type AuthManager struct {
DB *db.DBManager
Config *config.Config
Session *session.SessionManager
DB *db.DBManager
Config *config.Config
}
func NewMgr(db *db.DBManager, c *config.Config) *AuthManager {
session := session.NewMgr()
return &AuthManager{
DB: db,
Config: c,
Session: session,
}
return &AuthManager{
DB: db,
Config: c,
}
}
func (auth *AuthManager) AuthenticateUser(user, password string) (bool, model.User) {
// Search Objects
userByName := &model.User{}
userByName.Username = user
func (auth *AuthManager) AuthenticateUser(user, password string) (model.User, bool) {
// Find User by Username / Email
foundUser := &model.User{Username: user}
_, err := auth.DB.User(foundUser)
foundUser, err := auth.DB.User(userByName)
if errors.Is(err, gorm.ErrRecordNotFound) {
userByEmail := &model.User{}
userByEmail.Email = user
foundUser, err = auth.DB.User(userByEmail)
}
// By Username
if errors.Is(err, gorm.ErrRecordNotFound) {
foundUser = &model.User{Email: user}
_, err = auth.DB.User(foundUser)
}
// Error Checking
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn("[auth] User not found: ", user)
return false, foundUser
} else if err != nil {
log.Error(err)
return false, foundUser
}
// By Email
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Warn("[auth] User not found: ", user)
return *foundUser, false
} else if err != nil {
log.Error(err)
return *foundUser, false
}
log.Info("[auth] Authenticating user: ", foundUser.Username)
log.Info("[auth] Authenticating user: ", foundUser.Username)
// Determine Type
switch foundUser.AuthType {
case "Local":
return authenticateLocalUser(foundUser, password), foundUser
case "LDAP":
return authenticateLDAPUser(foundUser, password), foundUser
default:
return false, foundUser
}
}
func (auth *AuthManager) getRole(user model.User) string {
// TODO: Lookup role of user
return "User"
// Determine Type
switch foundUser.AuthType {
case "Local":
return *foundUser, authenticateLocalUser(*foundUser, password)
case "LDAP":
return *foundUser, authenticateLDAPUser(*foundUser, password)
default:
return *foundUser, false
}
}
func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token, error) {
byteRefreshJWT := []byte(refreshJWT)
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")
}
stringDeviceID := deviceID.String()
device, err := auth.DB.Device(&model.Device{ID: &stringDeviceID})
if err != nil {
return nil, err
}
// 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
// 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)),
)
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
}
if err != nil {
return nil, err
}
return verifiedToken, nil
return verifiedToken, nil
}
func (auth *AuthManager) CreateJWTRefreshToken(user model.User, device model.Device) (string, error) {
// Acquire Refresh Key
byteKey := []byte(*device.RefreshKey)
// 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
// 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
}
// 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
}
// 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
}
// 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
// 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`, auth.getRole(user)) // 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
// 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
}
// 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)
// 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
}
// 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
// Return Token
return string(signed), nil
}

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
package db
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 {
log.Info("[db] Creating media item: ", mediaItem.FileName)
err := dbm.db.Create(&mediaItem).Error
return err
func (dbm *DBManager) CreateMediaItem(mediaItem *model.MediaItem) error {
log.Debug("[db] Creating media item: ", mediaItem.FileName)
err := dbm.db.Create(mediaItem).Error
return err
}
func (dbm *DBManager) MediaItems(mediaItemFilter *model.MediaItem) ([]model.MediaItem, int64, error) {
var mediaItems []model.MediaItem
var count int64
var mediaItems []model.MediaItem
var count int64
err := dbm.db.Where(&mediaItemFilter).Find(&mediaItems).Count(&count).Error;
return mediaItems, count, err
err := dbm.db.Where(mediaItemFilter).Find(&mediaItems).Count(&count).Error
return mediaItems, count, err
}

View File

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