Merge pull request 'Enable GraphQL!' (#1) from graphql into master
Reviewed-on: #1
This commit is contained in:
commit
5072c9764f
99
cmd/main.go
99
cmd/main.go
@ -1,55 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"github.com/urfave/cli/v2"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"reichard.io/imagini/cmd/server"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"reichard.io/imagini/cmd/server"
|
||||
|
||||
"github.com/99designs/gqlgen/api"
|
||||
"github.com/99designs/gqlgen/codegen/config"
|
||||
"reichard.io/imagini/plugin"
|
||||
)
|
||||
|
||||
type UTCFormatter struct {
|
||||
log.Formatter
|
||||
log.Formatter
|
||||
}
|
||||
|
||||
func (u UTCFormatter) Format(e *log.Entry) ([]byte, error) {
|
||||
e.Time = e.Time.UTC()
|
||||
return u.Formatter.Format(e)
|
||||
e.Time = e.Time.UTC()
|
||||
return u.Formatter.Format(e)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFormatter(UTCFormatter{&log.TextFormatter{FullTimestamp: true}})
|
||||
log.SetFormatter(UTCFormatter{&log.TextFormatter{FullTimestamp: true}})
|
||||
|
||||
log.Info("Starting Imagini")
|
||||
app := &cli.App{
|
||||
Name: "Imagini",
|
||||
Usage: "A self hosted photo library.",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "serve",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Start Imagini web server.",
|
||||
Action: cmdServer,
|
||||
},
|
||||
},
|
||||
}
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
app := &cli.App{
|
||||
Name: "Imagini",
|
||||
Usage: "A self hosted photo library.",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "serve",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Start Imagini web server.",
|
||||
Action: cmdServer,
|
||||
},
|
||||
{
|
||||
Name: "generate",
|
||||
Usage: "generate graphql schema",
|
||||
Action: cmdGenerate,
|
||||
},
|
||||
},
|
||||
}
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdServer(ctx *cli.Context) error {
|
||||
server := server.NewServer()
|
||||
server.StartServer()
|
||||
log.Info("Starting Imagini Server")
|
||||
server := server.NewServer()
|
||||
server.StartServer()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
<-c
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
<-c
|
||||
|
||||
server.StopServer()
|
||||
os.Exit(0)
|
||||
server.StopServer()
|
||||
os.Exit(0)
|
||||
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdGenerate(ctx *cli.Context) error {
|
||||
log.Info("Generating Imagini Models")
|
||||
gqlgenConf, err := config.LoadConfigFromDefaultLocations()
|
||||
if err != nil {
|
||||
log.Panic("Failed to load config", err.Error())
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
log.Info("Generating Schema...")
|
||||
err = api.Generate(gqlgenConf,
|
||||
api.AddPlugin(plugin.New()),
|
||||
)
|
||||
log.Info("Schema Generation Done")
|
||||
if err != nil {
|
||||
log.Panic(err.Error())
|
||||
os.Exit(3)
|
||||
}
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
@ -1,58 +1,59 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
"context"
|
||||
"net/http"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"reichard.io/imagini/internal/db"
|
||||
"reichard.io/imagini/internal/api"
|
||||
"reichard.io/imagini/internal/auth"
|
||||
"reichard.io/imagini/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"reichard.io/imagini/internal/api"
|
||||
"reichard.io/imagini/internal/auth"
|
||||
"reichard.io/imagini/internal/config"
|
||||
"reichard.io/imagini/internal/db"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
API *api.API
|
||||
Auth *auth.AuthManager
|
||||
Config *config.Config
|
||||
Database *db.DBManager
|
||||
httpServer *http.Server
|
||||
API *api.API
|
||||
Auth *auth.AuthManager
|
||||
Config *config.Config
|
||||
Database *db.DBManager
|
||||
httpServer *http.Server
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
c := config.Load()
|
||||
db := db.NewMgr(c)
|
||||
auth := auth.NewMgr(db, c)
|
||||
api := api.NewApi(db, c, auth)
|
||||
c := config.Load()
|
||||
db := db.NewMgr(c)
|
||||
auth := auth.NewMgr(db, c)
|
||||
api := api.NewApi(db, c, auth)
|
||||
|
||||
return &Server{
|
||||
API: api,
|
||||
Auth: auth,
|
||||
Config: c,
|
||||
Database: db,
|
||||
}
|
||||
return &Server{
|
||||
API: api,
|
||||
Auth: auth,
|
||||
Config: c,
|
||||
Database: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) StartServer() {
|
||||
listenAddr := (":" + s.Config.ListenPort)
|
||||
listenAddr := (":" + s.Config.ListenPort)
|
||||
|
||||
s.httpServer = &http.Server{
|
||||
Handler: s.API.Router,
|
||||
Addr: listenAddr,
|
||||
}
|
||||
s.httpServer = &http.Server{
|
||||
Handler: s.API.Router,
|
||||
Addr: listenAddr,
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := s.httpServer.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Error("Error starting server ", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
err := s.httpServer.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Error("Error starting server ", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Server) StopServer() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
|
||||
defer cancel()
|
||||
s.httpServer.Shutdown(ctx)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
s.httpServer.Shutdown(ctx)
|
||||
}
|
||||
|
3
go.mod
3
go.mod
@ -3,17 +3,20 @@ module reichard.io/imagini
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.13.0
|
||||
github.com/codeon/govips v0.0.0-20200329201227-415341c0ce33 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20201216222538-db167117f483
|
||||
github.com/gabriel-vasile/mimetype v1.1.2
|
||||
github.com/google/uuid v1.1.5
|
||||
github.com/iancoleman/strcase v0.1.3
|
||||
github.com/lestrrat-go/jwx v1.0.8
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/sirupsen/logrus v1.7.0
|
||||
github.com/tus/tusd v1.4.0
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
github.com/vektah/gqlparser/v2 v2.1.0
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
|
||||
gorm.io/driver/sqlite v1.1.4
|
||||
|
53
go.sum
53
go.sum
@ -2,9 +2,16 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro=
|
||||
github.com/99designs/gqlgen v0.13.0 h1:haLTcUp3Vwp80xMVEg5KRNwzfUrgFdRmtBY8fuB8scA=
|
||||
github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
|
||||
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/aws/aws-sdk-go v1.20.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
@ -22,6 +29,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dsoprea/go-exif v0.0.0-20201216222538-db167117f483 h1:zJb7OUzMMSul61UUhYXWNOXc9nO1lexj3jsAgoDtCqg=
|
||||
@ -41,6 +49,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/gabriel-vasile/mimetype v1.1.2 h1:gaPnPcNor5aZSVCJVSGipcpbgMWiAAj9z182ocSGbHU=
|
||||
github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
|
||||
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
|
||||
@ -52,6 +61,7 @@ github.com/go-macaron/gzip v0.0.0-20200329073552-98214d7a897e/go.mod h1:1if9hBU2
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI=
|
||||
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
@ -77,10 +87,17 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/iancoleman/strcase v0.1.3 h1:dJBk1m2/qjL1twPLf68JND55vvivMupZ4wIzE8CTdBw=
|
||||
github.com/iancoleman/strcase v0.1.3/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
@ -98,6 +115,9 @@ github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eT
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.3 h1:2ABaTa5ifB1L90aoRMjaPa97p0WzzVe93Vggv8oZftw=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.3/go.mod h1:mU93bMXuG27/Y5erI5E9weqavpTX5qiVFZI4uXAX0xk=
|
||||
github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3 h1:e52qvXxpJPV/Kb2ovtuYgcRFjNmf9ntcn8BPIbpRM4k=
|
||||
@ -109,14 +129,24 @@ github.com/lestrrat-go/jwx v1.0.8/go.mod h1:6XJ5sxHF5U116AxYxeHfTnfsZRMgmeKY214z
|
||||
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10=
|
||||
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -131,11 +161,15 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
@ -148,6 +182,7 @@ github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:s
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@ -159,8 +194,13 @@ github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e h1:GSGeB9EAKY2spCABz6x
|
||||
github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
|
||||
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
|
||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns=
|
||||
github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
|
||||
github.com/vimeo/go-util v1.2.0/go.mod h1:s13SMDTSO7AjH1nbgp707mfN5JFIWUFDU5MDDuRRtKs=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
@ -186,6 +226,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -215,11 +257,13 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
@ -230,17 +274,22 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290 h1:NXNmtp0ToD36cui5IqWy95LC4Y6vT/4y3RnPxlQPinU=
|
||||
golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
|
||||
@ -261,6 +310,7 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
||||
gopkg.in/Acconut/lockfile.v1 v1.1.0/go.mod h1:6UCz3wJ8tSFUsPR6uP/j8uegEtDuEEqFxlpi0JI4Umw=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/h2non/gock.v1 v1.0.14/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
|
||||
gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
@ -270,6 +320,7 @@ gopkg.in/macaron.v1 v1.4.0/go.mod h1:uMZCFccv9yr5TipIalVOyAyZQuOH3OkmXvgcWwhJuP4
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@ -284,3 +335,5 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
|
||||
|
56
gqlgen.yml
Normal file
56
gqlgen.yml
Normal file
@ -0,0 +1,56 @@
|
||||
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
|
||||
schema:
|
||||
- graph/*.graphqls
|
||||
|
||||
# Where should the generated server code go?
|
||||
exec:
|
||||
filename: graph/generated/generated.go
|
||||
package: generated
|
||||
|
||||
# Uncomment to enable federation
|
||||
# federation:
|
||||
# filename: graph/generated/federation.go
|
||||
# package: generated
|
||||
|
||||
# Where should any generated models go?
|
||||
model:
|
||||
filename: graph/model/models_gen.go
|
||||
package: model
|
||||
|
||||
# Where should the resolver implementations go?
|
||||
resolver:
|
||||
layout: follow-schema
|
||||
dir: graph
|
||||
package: graph
|
||||
|
||||
# Optional: turn on use `gqlgen:"fieldName"` tags in your models
|
||||
# struct_tag: json
|
||||
|
||||
# Optional: turn on to use []Thing instead of []*Thing
|
||||
# omit_slice_element_pointers: false
|
||||
|
||||
# Optional: set to speed up generation time by not performing a final validation pass.
|
||||
# skip_validation: true
|
||||
|
||||
# gqlgen will search for any type names in the schema in these go packages
|
||||
# if they match it will use them, otherwise it will generate them.
|
||||
autobind:
|
||||
- "reichard.io/imagini/graph/model"
|
||||
|
||||
# This section declares type mapping between the GraphQL and go type systems
|
||||
#
|
||||
# The first line in each type will be used as defaults for resolver arguments and
|
||||
# modelgen, the others will be allowed when binding to fields. Configure them to
|
||||
# your liking
|
||||
models:
|
||||
ID:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.ID
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
||||
Int:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.Int
|
||||
- github.com/99designs/gqlgen/graphql.Int64
|
||||
- github.com/99designs/gqlgen/graphql.Int32
|
9712
graph/generated/generated.go
Normal file
9712
graph/generated/generated.go
Normal file
File diff suppressed because it is too large
Load Diff
120
graph/helpers.go
Normal file
120
graph/helpers.go
Normal file
@ -0,0 +1,120 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dsoprea/go-exif/v3"
|
||||
exifcommon "github.com/dsoprea/go-exif/v3/common"
|
||||
"github.com/google/uuid"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func getContextHTTP(ctx context.Context) (*http.ResponseWriter, *http.Request, error) {
|
||||
authContext := ctx.Value("auth").(*model.AuthContext)
|
||||
|
||||
resp := authContext.AuthResponse
|
||||
if resp == nil {
|
||||
return nil, nil, errors.New("Context Error")
|
||||
}
|
||||
|
||||
req := authContext.AuthRequest
|
||||
if resp == nil {
|
||||
return nil, nil, errors.New("Context Error")
|
||||
}
|
||||
|
||||
return resp, req, nil
|
||||
}
|
||||
|
||||
func getContextIDs(ctx context.Context) (string, string, error) {
|
||||
authContext := ctx.Value("auth").(*model.AuthContext)
|
||||
accessToken := *authContext.AccessToken
|
||||
|
||||
uid, ok := accessToken.Get("sub")
|
||||
if !ok {
|
||||
return "", "", errors.New("Context Error")
|
||||
}
|
||||
|
||||
did, ok := accessToken.Get("sub")
|
||||
if !ok {
|
||||
return "", "", errors.New("Context Error")
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(uid.(string))
|
||||
if err != nil {
|
||||
return "", "", errors.New("Context Error")
|
||||
}
|
||||
|
||||
deviceID, err := uuid.Parse(did.(string))
|
||||
if err != nil {
|
||||
return "", "", errors.New("Context Error")
|
||||
}
|
||||
|
||||
return userID.String(), deviceID.String(), nil
|
||||
}
|
||||
|
||||
func deriveDeviceType(r *http.Request) model.DeviceType {
|
||||
userAgent := strings.ToLower(r.Header.Get("User-Agent"))
|
||||
if strings.Contains(userAgent, "ios-imagini") {
|
||||
return model.DeviceTypeIOs
|
||||
} else if strings.Contains(userAgent, "android-imagini") {
|
||||
return model.DeviceTypeAndroid
|
||||
} else if strings.Contains(userAgent, "chrome") {
|
||||
return model.DeviceTypeChrome
|
||||
} else if strings.Contains(userAgent, "firefox") {
|
||||
return model.DeviceTypeFirefox
|
||||
} else if strings.Contains(userAgent, "msie") {
|
||||
return model.DeviceTypeInternetExplorer
|
||||
} else if strings.Contains(userAgent, "edge") {
|
||||
return model.DeviceTypeEdge
|
||||
} else if strings.Contains(userAgent, "safari") {
|
||||
return model.DeviceTypeSafari
|
||||
}
|
||||
return model.DeviceTypeUnknown
|
||||
}
|
||||
|
||||
func mediaItemFromEXIFData(filePath string) (*model.MediaItem, error) {
|
||||
rawExif, err := exif.SearchFileAndExtractExif(filePath)
|
||||
entries, _, err := exif.GetFlatExifData(rawExif, nil)
|
||||
|
||||
decLong := float64(1)
|
||||
decLat := float64(1)
|
||||
|
||||
mediaItem := &model.MediaItem{}
|
||||
for _, v := range entries {
|
||||
if v.TagName == "DateTimeOriginal" {
|
||||
formattedTime, _ := time.Parse("2006:01:02 15:04:05", v.Formatted)
|
||||
mediaItem.ExifDate = &formattedTime
|
||||
} else if v.TagName == "GPSLatitude" {
|
||||
latStruct := v.Value.([]exifcommon.Rational)
|
||||
decLat *= deriveDecimalCoordinate(
|
||||
latStruct[0].Numerator/latStruct[0].Denominator,
|
||||
latStruct[1].Numerator/latStruct[1].Denominator,
|
||||
float64(latStruct[2].Numerator)/float64(latStruct[2].Denominator),
|
||||
)
|
||||
} else if v.TagName == "GPSLongitude" {
|
||||
longStruct := v.Value.([]exifcommon.Rational)
|
||||
decLong *= deriveDecimalCoordinate(
|
||||
longStruct[0].Numerator/longStruct[0].Denominator,
|
||||
longStruct[1].Numerator/longStruct[1].Denominator,
|
||||
float64(longStruct[2].Numerator)/float64(longStruct[2].Denominator),
|
||||
)
|
||||
} else if v.TagName == "GPSLatitudeRef" && v.Formatted == "S" {
|
||||
decLat *= -1
|
||||
} else if v.TagName == "GPSLongitudeRef" && v.Formatted == "W" {
|
||||
decLong *= -1
|
||||
}
|
||||
}
|
||||
|
||||
mediaItem.Latitude = &decLat
|
||||
mediaItem.Longitude = &decLong
|
||||
|
||||
return mediaItem, err
|
||||
}
|
||||
|
||||
func deriveDecimalCoordinate(degrees, minutes uint32, seconds float64) float64 {
|
||||
return float64(degrees) + (float64(minutes) / 60) + (seconds / 3600)
|
||||
}
|
13
graph/model/models_auth.go
Normal file
13
graph/model/models_auth.go
Normal file
@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
)
|
||||
|
||||
type AuthContext struct {
|
||||
AccessToken *jwt.Token
|
||||
AuthResponse *http.ResponseWriter
|
||||
AuthRequest *http.Request
|
||||
}
|
36
graph/model/models_db.go
Normal file
36
graph/model/models_db.go
Normal file
@ -0,0 +1,36 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
newID := uuid.New().String()
|
||||
u.ID = newID
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Album) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
newID := uuid.New().String()
|
||||
a.ID = newID
|
||||
return
|
||||
}
|
||||
|
||||
func (m *MediaItem) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
newID := uuid.New().String()
|
||||
m.ID = newID
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Tag) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
newID := uuid.New().String()
|
||||
t.ID = newID
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
newID := uuid.New().String()
|
||||
d.ID = newID
|
||||
return
|
||||
}
|
465
graph/model/models_gen.go
Normal file
465
graph/model/models_gen.go
Normal file
@ -0,0 +1,465 @@
|
||||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
)
|
||||
|
||||
type Album struct {
|
||||
ID string `json:"id" gorm:"primaryKey;not null"`
|
||||
CreatedAt *time.Time `json:"createdAt" `
|
||||
UpdatedAt *time.Time `json:"updatedAt" `
|
||||
Name string `json:"name" gorm:"unique;not null"`
|
||||
UserID string `json:"userID" gorm:"not null"`
|
||||
}
|
||||
|
||||
type AlbumFilter struct {
|
||||
ID *IDFilter `json:"id" `
|
||||
CreatedAt *TimeFilter `json:"createdAt" `
|
||||
UpdatedAt *TimeFilter `json:"updatedAt" `
|
||||
Name *StringFilter `json:"name" `
|
||||
}
|
||||
|
||||
type AlbumResponse struct {
|
||||
Data []*Album `json:"data" `
|
||||
Page *PageResponse `json:"page" `
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Result AuthResult `json:"result" `
|
||||
Device *Device `json:"device" `
|
||||
Error *string `json:"error" `
|
||||
}
|
||||
|
||||
type AuthTypeFilter struct {
|
||||
EqualTo *AuthType `json:"equalTo" `
|
||||
NotEqualTo *AuthType `json:"notEqualTo" `
|
||||
}
|
||||
|
||||
type BooleanFilter struct {
|
||||
EqualTo *bool `json:"equalTo" `
|
||||
NotEqualTo *bool `json:"notEqualTo" `
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID string `json:"id" gorm:"primaryKey;not null"`
|
||||
CreatedAt *time.Time `json:"createdAt" `
|
||||
UpdatedAt *time.Time `json:"updatedAt" `
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Type DeviceType `json:"type" gorm:"default:Unknown;not null"`
|
||||
RefreshKey *string `json:"refreshKey" `
|
||||
UserID string `json:"userID" gorm:"not null"`
|
||||
}
|
||||
|
||||
type DeviceFilter struct {
|
||||
ID *IDFilter `json:"id" `
|
||||
CreatedAt *TimeFilter `json:"createdAt" `
|
||||
UpdatedAt *TimeFilter `json:"updatedAt" `
|
||||
Name *StringFilter `json:"name" `
|
||||
Type *DeviceTypeFilter `json:"type" `
|
||||
}
|
||||
|
||||
type DeviceResponse struct {
|
||||
Data []*Device `json:"data" `
|
||||
Page *PageResponse `json:"page" `
|
||||
}
|
||||
|
||||
type DeviceTypeFilter struct {
|
||||
EqualTo *DeviceType `json:"equalTo" `
|
||||
NotEqualTo *DeviceType `json:"notEqualTo" `
|
||||
}
|
||||
|
||||
type FloatFilter struct {
|
||||
EqualTo *float64 `json:"equalTo" `
|
||||
NotEqualTo *float64 `json:"notEqualTo" `
|
||||
LessThan *float64 `json:"lessThan" `
|
||||
LessThanOrEqualTo *float64 `json:"lessThanOrEqualTo" `
|
||||
GreaterThan *float64 `json:"greaterThan" `
|
||||
GreaterThanOrEqualTo *float64 `json:"greaterThanOrEqualTo" `
|
||||
}
|
||||
|
||||
type IDFilter struct {
|
||||
EqualTo *string `json:"equalTo" `
|
||||
NotEqualTo *string `json:"notEqualTo" `
|
||||
}
|
||||
|
||||
type IntFilter struct {
|
||||
EqualTo *int `json:"equalTo" `
|
||||
NotEqualTo *int `json:"notEqualTo" `
|
||||
LessThan *int `json:"lessThan" `
|
||||
LessThanOrEqualTo *int `json:"lessThanOrEqualTo" `
|
||||
GreaterThan *int `json:"greaterThan" `
|
||||
GreaterThanOrEqualTo *int `json:"greaterThanOrEqualTo" `
|
||||
}
|
||||
|
||||
type MediaItem struct {
|
||||
ID string `json:"id" gorm:"primaryKey;not null"`
|
||||
CreatedAt *time.Time `json:"createdAt" `
|
||||
UpdatedAt *time.Time `json:"updatedAt" `
|
||||
ExifDate *time.Time `json:"exifDate" `
|
||||
Latitude *float64 `json:"latitude" gorm:"precision:5"`
|
||||
Longitude *float64 `json:"longitude" gorm:"precision:5"`
|
||||
IsVideo bool `json:"isVideo" gorm:"default:false;not null"`
|
||||
FileName string `json:"fileName" gorm:"not null"`
|
||||
OrigName string `json:"origName" gorm:"not null"`
|
||||
Tags []*Tag `json:"tags" gorm:"many2many:media_tags;foreignKey:ID,UserID;References:ID"`
|
||||
Albums []*Album `json:"albums" gorm:"many2many:media_albums;foreignKey:ID,UserID;Refrences:ID"`
|
||||
UserID string `json:"userID" gorm:"not null"`
|
||||
}
|
||||
|
||||
type MediaItemFilter struct {
|
||||
ID *IDFilter `json:"id" `
|
||||
CreatedAt *TimeFilter `json:"createdAt" `
|
||||
UpdatedAt *TimeFilter `json:"updatedAt" `
|
||||
ExifDate *TimeFilter `json:"exifDate" `
|
||||
Latitude *FloatFilter `json:"latitude" `
|
||||
Longitude *FloatFilter `json:"longitude" `
|
||||
IsVideo *BooleanFilter `json:"isVideo" `
|
||||
OrigName *StringFilter `json:"origName" `
|
||||
Tags *TagFilter `json:"tags" `
|
||||
Albums *AlbumFilter `json:"albums" `
|
||||
}
|
||||
|
||||
type MediaItemResponse struct {
|
||||
Data []*MediaItem `json:"data" `
|
||||
Page *PageResponse `json:"page" `
|
||||
}
|
||||
|
||||
type NewAlbum struct {
|
||||
Name string `json:"name" `
|
||||
}
|
||||
|
||||
type NewMediaItem struct {
|
||||
File graphql.Upload `json:"file" `
|
||||
Tags []string `json:"tags" `
|
||||
Albums []string `json:"albums" `
|
||||
}
|
||||
|
||||
type NewTag struct {
|
||||
Name string `json:"name" `
|
||||
}
|
||||
|
||||
type NewUser struct {
|
||||
Email string `json:"email" `
|
||||
Username string `json:"username" `
|
||||
FirstName *string `json:"firstName" `
|
||||
LastName *string `json:"lastName" `
|
||||
Role Role `json:"role" `
|
||||
AuthType AuthType `json:"authType" `
|
||||
Password *string `json:"password" `
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
By *string `json:"by" `
|
||||
Direction *OrderDirection `json:"direction" `
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Size *int `json:"size" `
|
||||
Page *int `json:"page" `
|
||||
}
|
||||
|
||||
type PageResponse struct {
|
||||
Size int `json:"size" `
|
||||
Page int `json:"page" `
|
||||
Total int `json:"total" `
|
||||
}
|
||||
|
||||
type RoleFilter struct {
|
||||
EqualTo *Role `json:"equalTo" `
|
||||
NotEqualTo *Role `json:"notEqualTo" `
|
||||
}
|
||||
|
||||
type StringFilter struct {
|
||||
EqualTo *string `json:"equalTo" `
|
||||
NotEqualTo *string `json:"notEqualTo" `
|
||||
StartsWith *string `json:"startsWith" `
|
||||
NotStartsWith *string `json:"notStartsWith" `
|
||||
EndsWith *string `json:"endsWith" `
|
||||
NotEndsWith *string `json:"notEndsWith" `
|
||||
Contains *string `json:"contains" `
|
||||
NotContains *string `json:"notContains" `
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
ID string `json:"id" gorm:"primaryKey;not null"`
|
||||
CreatedAt *time.Time `json:"createdAt" `
|
||||
UpdatedAt *time.Time `json:"updatedAt" `
|
||||
Name string `json:"name" gorm:"unique;not null"`
|
||||
UserID string `json:"userID" gorm:"not null"`
|
||||
}
|
||||
|
||||
type TagFilter struct {
|
||||
ID *IDFilter `json:"id" `
|
||||
CreatedAt *TimeFilter `json:"createdAt" `
|
||||
UpdatedAt *TimeFilter `json:"updatedAt" `
|
||||
Name *StringFilter `json:"name" `
|
||||
}
|
||||
|
||||
type TagResponse struct {
|
||||
Data []*Tag `json:"data" `
|
||||
Page *PageResponse `json:"page" `
|
||||
}
|
||||
|
||||
type TimeFilter struct {
|
||||
EqualTo *time.Time `json:"equalTo" `
|
||||
NotEqualTo *time.Time `json:"notEqualTo" `
|
||||
LessThan *time.Time `json:"lessThan" `
|
||||
LessThanOrEqualTo *time.Time `json:"lessThanOrEqualTo" `
|
||||
GreaterThan *time.Time `json:"greaterThan" `
|
||||
GreaterThanOrEqualTo *time.Time `json:"greaterThanOrEqualTo" `
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id" gorm:"primaryKey;not null"`
|
||||
CreatedAt *time.Time `json:"createdAt" `
|
||||
UpdatedAt *time.Time `json:"updatedAt" `
|
||||
Email string `json:"email" gorm:"not null;unique"`
|
||||
Username string `json:"username" gorm:"not null;unique"`
|
||||
FirstName *string `json:"firstName" `
|
||||
LastName *string `json:"lastName" `
|
||||
Role Role `json:"role" gorm:"default:User;not null"`
|
||||
AuthType AuthType `json:"authType" gorm:"default:Local;not null"`
|
||||
Password *string `json:"password" `
|
||||
Devices []*Device `json:"devices" gorm:"foreignKey:UserID"`
|
||||
MediaItems []*MediaItem `json:"mediaItems" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
type UserFilter struct {
|
||||
ID *IDFilter `json:"id" `
|
||||
CreatedAt *TimeFilter `json:"createdAt" `
|
||||
UpdatedAt *TimeFilter `json:"updatedAt" `
|
||||
Username *StringFilter `json:"username" `
|
||||
FirstName *StringFilter `json:"firstName" `
|
||||
LastName *StringFilter `json:"lastName" `
|
||||
Role *RoleFilter `json:"role" `
|
||||
AuthType *AuthTypeFilter `json:"authType" `
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
Data []*User `json:"data" `
|
||||
Page *PageResponse `json:"page" `
|
||||
}
|
||||
|
||||
type AuthResult string
|
||||
|
||||
const (
|
||||
AuthResultSuccess AuthResult = "Success"
|
||||
AuthResultFailure AuthResult = "Failure"
|
||||
)
|
||||
|
||||
var AllAuthResult = []AuthResult{
|
||||
AuthResultSuccess,
|
||||
AuthResultFailure,
|
||||
}
|
||||
|
||||
func (e AuthResult) IsValid() bool {
|
||||
switch e {
|
||||
case AuthResultSuccess, AuthResultFailure:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e AuthResult) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *AuthResult) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = AuthResult(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid AuthResult", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e AuthResult) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type AuthType string
|
||||
|
||||
const (
|
||||
AuthTypeLocal AuthType = "Local"
|
||||
AuthTypeLdap AuthType = "LDAP"
|
||||
)
|
||||
|
||||
var AllAuthType = []AuthType{
|
||||
AuthTypeLocal,
|
||||
AuthTypeLdap,
|
||||
}
|
||||
|
||||
func (e AuthType) IsValid() bool {
|
||||
switch e {
|
||||
case AuthTypeLocal, AuthTypeLdap:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e AuthType) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *AuthType) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = AuthType(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid AuthType", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e AuthType) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type DeviceType string
|
||||
|
||||
const (
|
||||
DeviceTypeIOs DeviceType = "iOS"
|
||||
DeviceTypeAndroid DeviceType = "Android"
|
||||
DeviceTypeChrome DeviceType = "Chrome"
|
||||
DeviceTypeFirefox DeviceType = "Firefox"
|
||||
DeviceTypeInternetExplorer DeviceType = "InternetExplorer"
|
||||
DeviceTypeEdge DeviceType = "Edge"
|
||||
DeviceTypeSafari DeviceType = "Safari"
|
||||
DeviceTypeUnknown DeviceType = "Unknown"
|
||||
)
|
||||
|
||||
var AllDeviceType = []DeviceType{
|
||||
DeviceTypeIOs,
|
||||
DeviceTypeAndroid,
|
||||
DeviceTypeChrome,
|
||||
DeviceTypeFirefox,
|
||||
DeviceTypeInternetExplorer,
|
||||
DeviceTypeEdge,
|
||||
DeviceTypeSafari,
|
||||
DeviceTypeUnknown,
|
||||
}
|
||||
|
||||
func (e DeviceType) IsValid() bool {
|
||||
switch e {
|
||||
case DeviceTypeIOs, DeviceTypeAndroid, DeviceTypeChrome, DeviceTypeFirefox, DeviceTypeInternetExplorer, DeviceTypeEdge, DeviceTypeSafari, DeviceTypeUnknown:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e DeviceType) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *DeviceType) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = DeviceType(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid DeviceType", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e DeviceType) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type OrderDirection string
|
||||
|
||||
const (
|
||||
OrderDirectionAsc OrderDirection = "ASC"
|
||||
OrderDirectionDesc OrderDirection = "DESC"
|
||||
)
|
||||
|
||||
var AllOrderDirection = []OrderDirection{
|
||||
OrderDirectionAsc,
|
||||
OrderDirectionDesc,
|
||||
}
|
||||
|
||||
func (e OrderDirection) IsValid() bool {
|
||||
switch e {
|
||||
case OrderDirectionAsc, OrderDirectionDesc:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e OrderDirection) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *OrderDirection) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = OrderDirection(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid OrderDirection", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e OrderDirection) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleAdmin Role = "Admin"
|
||||
RoleUser Role = "User"
|
||||
)
|
||||
|
||||
var AllRole = []Role{
|
||||
RoleAdmin,
|
||||
RoleUser,
|
||||
}
|
||||
|
||||
func (e Role) IsValid() bool {
|
||||
switch e {
|
||||
case RoleAdmin, RoleUser:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e Role) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *Role) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = Role(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid Role", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e Role) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
17
graph/resolver.go
Normal file
17
graph/resolver.go
Normal file
@ -0,0 +1,17 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"reichard.io/imagini/internal/auth"
|
||||
"reichard.io/imagini/internal/config"
|
||||
"reichard.io/imagini/internal/db"
|
||||
)
|
||||
|
||||
// This file will not be regenerated automatically.
|
||||
//
|
||||
// It serves as dependency injection for your app, add any dependencies you require here.
|
||||
|
||||
type Resolver struct {
|
||||
Config *config.Config
|
||||
Auth *auth.AuthManager
|
||||
DB *db.DBManager
|
||||
}
|
384
graph/schema.graphqls
Normal file
384
graph/schema.graphqls
Normal file
@ -0,0 +1,384 @@
|
||||
|
||||
# https://gqlgen.com/reference/scalars/
|
||||
scalar Time
|
||||
scalar Upload
|
||||
|
||||
# https://gqlgen.com/reference/directives/
|
||||
directive @hasMinRole(role: Role!) on FIELD_DEFINITION
|
||||
directive @isPrivate on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
|
||||
|
||||
directive @meta(
|
||||
gorm: String,
|
||||
) on OBJECT | FIELD_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION | ENUM | INPUT_OBJECT | ARGUMENT_DEFINITION
|
||||
|
||||
enum Role {
|
||||
Admin
|
||||
User
|
||||
}
|
||||
|
||||
enum DeviceType {
|
||||
iOS
|
||||
Android
|
||||
Chrome
|
||||
Firefox
|
||||
InternetExplorer
|
||||
Edge
|
||||
Safari
|
||||
Unknown
|
||||
}
|
||||
|
||||
enum AuthType {
|
||||
Local
|
||||
LDAP
|
||||
}
|
||||
|
||||
enum OrderDirection {
|
||||
ASC
|
||||
DESC
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ---------------------- Authentication ----------------------
|
||||
# ------------------------------------------------------------
|
||||
|
||||
enum AuthResult {
|
||||
Success
|
||||
Failure
|
||||
}
|
||||
|
||||
type AuthResponse {
|
||||
result: AuthResult!
|
||||
device: Device
|
||||
error: String
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ----------------------- Type Filters -----------------------
|
||||
# ------------------------------------------------------------
|
||||
|
||||
input TimeFilter {
|
||||
equalTo: Time
|
||||
notEqualTo: Time
|
||||
lessThan: Time
|
||||
lessThanOrEqualTo: Time
|
||||
greaterThan: Time
|
||||
greaterThanOrEqualTo: Time
|
||||
}
|
||||
|
||||
input IntFilter {
|
||||
equalTo: Int
|
||||
notEqualTo: Int
|
||||
lessThan: Int
|
||||
lessThanOrEqualTo: Int
|
||||
greaterThan: Int
|
||||
greaterThanOrEqualTo: Int
|
||||
}
|
||||
|
||||
input FloatFilter {
|
||||
equalTo: Float
|
||||
notEqualTo: Float
|
||||
lessThan: Float
|
||||
lessThanOrEqualTo: Float
|
||||
greaterThan: Float
|
||||
greaterThanOrEqualTo: Float
|
||||
}
|
||||
|
||||
input BooleanFilter {
|
||||
equalTo: Boolean
|
||||
notEqualTo: Boolean
|
||||
}
|
||||
|
||||
input IDFilter {
|
||||
equalTo: ID
|
||||
notEqualTo: ID
|
||||
}
|
||||
|
||||
input StringFilter {
|
||||
equalTo: String
|
||||
notEqualTo: String
|
||||
startsWith: String
|
||||
notStartsWith: String
|
||||
endsWith: String
|
||||
notEndsWith: String
|
||||
contains: String
|
||||
notContains: String
|
||||
}
|
||||
|
||||
input RoleFilter {
|
||||
equalTo: Role
|
||||
notEqualTo: Role
|
||||
}
|
||||
|
||||
input DeviceTypeFilter {
|
||||
equalTo: DeviceType
|
||||
notEqualTo: DeviceType
|
||||
}
|
||||
|
||||
input AuthTypeFilter {
|
||||
equalTo: AuthType
|
||||
notEqualTo: AuthType
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# -------------------- Object Definitions --------------------
|
||||
# ------------------------------------------------------------
|
||||
|
||||
type User {
|
||||
id: ID! @meta(gorm: "primaryKey;not null")
|
||||
createdAt: Time
|
||||
updatedAt: Time
|
||||
email: String! @meta(gorm: "not null;unique")
|
||||
username: String! @meta(gorm: "not null;unique")
|
||||
firstName: String
|
||||
lastName: String
|
||||
role: Role! @meta(gorm: "default:User;not null")
|
||||
authType: AuthType! @meta(gorm: "default:Local;not null")
|
||||
password: String @isPrivate
|
||||
devices: [Device!] @meta(gorm: "foreignKey:UserID")
|
||||
mediaItems: [MediaItem!] @meta(gorm: "foreignKey:UserID")
|
||||
}
|
||||
|
||||
type Device {
|
||||
id: ID! @meta(gorm: "primaryKey;not null")
|
||||
createdAt: Time
|
||||
updatedAt: Time
|
||||
name: String! @meta(gorm: "not null")
|
||||
type: DeviceType! @meta(gorm: "default:Unknown;not null")
|
||||
refreshKey: String @isPrivate
|
||||
userID: ID! @meta(gorm: "not null")
|
||||
}
|
||||
|
||||
type MediaItem {
|
||||
id: ID! @meta(gorm: "primaryKey;not null")
|
||||
createdAt: Time
|
||||
updatedAt: Time
|
||||
exifDate: Time
|
||||
latitude: Float @meta(gorm: "precision:5")
|
||||
longitude: Float @meta(gorm: "precision:5")
|
||||
isVideo: Boolean! @meta(gorm: "default:false;not null")
|
||||
fileName: String! @meta(gorm: "not null")
|
||||
origName: String! @meta(gorm: "not null")
|
||||
tags: [Tag] @meta(gorm: "many2many:media_tags;foreignKey:ID,UserID;References:ID")
|
||||
albums: [Album] @meta(gorm: "many2many:media_albums;foreignKey:ID,UserID;Refrences:ID")
|
||||
userID: ID! @meta(gorm: "not null")
|
||||
}
|
||||
|
||||
type Tag {
|
||||
id: ID! @meta(gorm: "primaryKey;not null")
|
||||
createdAt: Time
|
||||
updatedAt: Time
|
||||
name: String! @meta(gorm: "unique;not null")
|
||||
userID: ID! @meta(gorm: "not null")
|
||||
}
|
||||
|
||||
type Album {
|
||||
id: ID! @meta(gorm: "primaryKey;not null")
|
||||
createdAt: Time
|
||||
updatedAt: Time
|
||||
name: String! @meta(gorm: "unique;not null")
|
||||
userID: ID! @meta(gorm: "not null")
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ---------------------- Object Filters ----------------------
|
||||
# ------------------------------------------------------------
|
||||
|
||||
input UserFilter {
|
||||
id: IDFilter
|
||||
createdAt: TimeFilter
|
||||
updatedAt: TimeFilter
|
||||
username: StringFilter
|
||||
firstName: StringFilter
|
||||
lastName: StringFilter
|
||||
role: RoleFilter
|
||||
authType: AuthTypeFilter
|
||||
|
||||
# and: UserFilter
|
||||
# or: UserFilter
|
||||
}
|
||||
|
||||
input MediaItemFilter {
|
||||
id: IDFilter
|
||||
createdAt: TimeFilter
|
||||
updatedAt: TimeFilter
|
||||
exifDate: TimeFilter
|
||||
latitude: FloatFilter
|
||||
longitude: FloatFilter
|
||||
isVideo: BooleanFilter
|
||||
origName: StringFilter
|
||||
tags: TagFilter
|
||||
albums: AlbumFilter
|
||||
|
||||
# and: MediaItemFilter
|
||||
# or: MediaItemFilter
|
||||
}
|
||||
|
||||
input DeviceFilter {
|
||||
id: IDFilter
|
||||
createdAt: TimeFilter
|
||||
updatedAt: TimeFilter
|
||||
name: StringFilter
|
||||
type: DeviceTypeFilter
|
||||
|
||||
# and: MediaItemFilter
|
||||
# or: MediaItemFilter
|
||||
}
|
||||
|
||||
input TagFilter {
|
||||
id: IDFilter
|
||||
createdAt: TimeFilter
|
||||
updatedAt: TimeFilter
|
||||
name: StringFilter
|
||||
|
||||
# and: MediaItemFilter
|
||||
# or: MediaItemFilter
|
||||
}
|
||||
|
||||
input AlbumFilter {
|
||||
id: IDFilter
|
||||
createdAt: TimeFilter
|
||||
updatedAt: TimeFilter
|
||||
name: StringFilter
|
||||
|
||||
# and: MediaItemFilter
|
||||
# or: MediaItemFilter
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# -------------------------- Inputs --------------------------
|
||||
# ------------------------------------------------------------
|
||||
|
||||
input NewUser {
|
||||
email: String!
|
||||
username: String!
|
||||
firstName: String
|
||||
lastName: String
|
||||
role: Role!
|
||||
authType: AuthType!
|
||||
password: String
|
||||
}
|
||||
|
||||
input NewMediaItem {
|
||||
file: Upload!
|
||||
tags: [ID!]
|
||||
albums: [ID!]
|
||||
}
|
||||
|
||||
input NewTag {
|
||||
name: String!
|
||||
}
|
||||
|
||||
input NewAlbum {
|
||||
name: String!
|
||||
}
|
||||
|
||||
input Page {
|
||||
size: Int
|
||||
page: Int
|
||||
}
|
||||
|
||||
input Order {
|
||||
by: String
|
||||
direction: OrderDirection
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ------------------------ Responses -------------------------
|
||||
# ------------------------------------------------------------
|
||||
|
||||
type PageResponse {
|
||||
size: Int!
|
||||
page: Int!
|
||||
total: Int!
|
||||
}
|
||||
|
||||
type MediaItemResponse {
|
||||
data: [MediaItem]
|
||||
page: PageResponse!
|
||||
}
|
||||
|
||||
type UserResponse {
|
||||
data: [User]
|
||||
page: PageResponse!
|
||||
}
|
||||
|
||||
type DeviceResponse {
|
||||
data: [Device]
|
||||
page: PageResponse!
|
||||
}
|
||||
|
||||
type TagResponse {
|
||||
data: [Tag]
|
||||
page: PageResponse!
|
||||
}
|
||||
|
||||
type AlbumResponse {
|
||||
data: [Album]
|
||||
page: PageResponse!
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# --------------------- Query & Mutations --------------------
|
||||
# ------------------------------------------------------------
|
||||
|
||||
type Query {
|
||||
# Authentication
|
||||
login(
|
||||
user: String!
|
||||
password: String!
|
||||
deviceID: ID
|
||||
): AuthResponse!
|
||||
logout: AuthResponse! @hasMinRole(role: User)
|
||||
|
||||
# Single Item
|
||||
mediaItem(
|
||||
id: ID!
|
||||
): MediaItem! @hasMinRole(role: User)
|
||||
device(
|
||||
id: ID!
|
||||
): Device! @hasMinRole(role: User)
|
||||
album(
|
||||
id: ID!
|
||||
): Album! @hasMinRole(role: User)
|
||||
user(
|
||||
id: ID!
|
||||
): User! @hasMinRole(role: Admin)
|
||||
tag(
|
||||
id: ID!
|
||||
): Tag! @hasMinRole(role: User)
|
||||
me: User! @hasMinRole(role: User)
|
||||
|
||||
# All
|
||||
mediaItems(
|
||||
filter: MediaItemFilter
|
||||
page: Page
|
||||
order: Order
|
||||
): MediaItemResponse! @hasMinRole(role: User)
|
||||
devices(
|
||||
filter: DeviceFilter
|
||||
page: Page
|
||||
order: Order
|
||||
): DeviceResponse! @hasMinRole(role: User)
|
||||
albums(
|
||||
filter: AlbumFilter
|
||||
page: Page
|
||||
order: Order
|
||||
): AlbumResponse! @hasMinRole(role: User)
|
||||
tags(
|
||||
filter: TagFilter
|
||||
page: Page
|
||||
order: Order
|
||||
): TagResponse! @hasMinRole(role: User)
|
||||
users(
|
||||
filter: UserFilter
|
||||
page: Page
|
||||
order: Order
|
||||
): UserResponse! @hasMinRole(role: Admin)
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createMediaItem(input: NewMediaItem!): MediaItem! @hasMinRole(role: User)
|
||||
createAlbum(input: NewAlbum!): Album! @hasMinRole(role: User)
|
||||
createTag(input: NewTag!): Tag! @hasMinRole(role: User)
|
||||
createUser(input: NewUser!): User! @hasMinRole(role: Admin)
|
||||
}
|
437
graph/schema.resolvers.go
Normal file
437
graph/schema.resolvers.go
Normal file
@ -0,0 +1,437 @@
|
||||
package graph
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/google/uuid"
|
||||
"reichard.io/imagini/graph/generated"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) CreateMediaItem(ctx context.Context, input model.NewMediaItem) (*model.MediaItem, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// File header placeholder
|
||||
fileHeader := make([]byte, 64)
|
||||
|
||||
// Copy headers into the buffer
|
||||
if _, err := input.File.File.Read(fileHeader); err != nil {
|
||||
return nil, errors.New("Upload Failed")
|
||||
}
|
||||
|
||||
// Determine media type
|
||||
fileMime := mimetype.Detect(fileHeader)
|
||||
contentType := fileMime.String()
|
||||
var isVideo bool
|
||||
if strings.HasPrefix(contentType, "image/") {
|
||||
isVideo = false
|
||||
} else if strings.HasPrefix(contentType, "video/") {
|
||||
isVideo = true
|
||||
} else {
|
||||
return nil, errors.New("Upload Failed")
|
||||
}
|
||||
|
||||
// Derive Folder & File Path
|
||||
mediaItemID := uuid.New().String()
|
||||
fileName := mediaItemID + fileMime.Extension()
|
||||
folderPath := path.Join("/" + r.Config.DataPath + "/media/" + userID)
|
||||
os.MkdirAll(folderPath, 0700)
|
||||
filePath := path.Join(folderPath + "/" + fileName)
|
||||
|
||||
// Create File
|
||||
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return nil, errors.New("Upload Failed")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Copy header to file
|
||||
_, err = io.Copy(f, bytes.NewReader(fileHeader))
|
||||
if err != nil {
|
||||
return nil, errors.New("Upload Failed")
|
||||
}
|
||||
|
||||
// Copy remaining file
|
||||
_, err = io.Copy(f, input.File.File)
|
||||
if err != nil {
|
||||
return nil, errors.New("Upload Failed")
|
||||
}
|
||||
|
||||
// Create MediaItem From EXIF Data
|
||||
mediaItem, err := mediaItemFromEXIFData(filePath)
|
||||
if err != nil {
|
||||
return nil, errors.New("Upload Failed")
|
||||
}
|
||||
|
||||
// Add Additional MediaItem Fields
|
||||
mediaItem.ID = mediaItemID
|
||||
mediaItem.UserID = userID
|
||||
mediaItem.IsVideo = isVideo
|
||||
mediaItem.FileName = fileName
|
||||
mediaItem.OrigName = input.File.Filename
|
||||
|
||||
// Create MediaItem in DB
|
||||
err = r.DB.CreateMediaItem(mediaItem)
|
||||
if err != nil {
|
||||
return nil, errors.New("Upload Failed")
|
||||
}
|
||||
|
||||
// Success
|
||||
return mediaItem, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateAlbum(ctx context.Context, input model.NewAlbum) (*model.Album, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
album := &model.Album{
|
||||
Name: input.Name,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
err = r.DB.CreateAlbum(album)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return album, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateTag(ctx context.Context, input model.NewTag) (*model.Tag, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tag := &model.Tag{
|
||||
Name: input.Name,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
err = r.DB.CreateTag(tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
|
||||
user := &model.User{
|
||||
Email: input.Email,
|
||||
Username: input.Username,
|
||||
FirstName: input.FirstName,
|
||||
LastName: input.LastName,
|
||||
Role: input.Role,
|
||||
AuthType: input.AuthType,
|
||||
Password: input.Password,
|
||||
}
|
||||
|
||||
err := r.DB.CreateUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Login(ctx context.Context, user string, password string, deviceID *string) (*model.AuthResponse, error) {
|
||||
// Acquire Context
|
||||
resp, req, err := getContextHTTP(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Clear All Cookies By Default
|
||||
accessCookie := http.Cookie{Name: "AccessToken", Path: "/", HttpOnly: true, MaxAge: -1, Expires: time.Now().Add(-100 * time.Hour)}
|
||||
refreshCookie := http.Cookie{Name: "RefreshToken", Path: "/", HttpOnly: true, MaxAge: -1, Expires: time.Now().Add(-100 * time.Hour)}
|
||||
http.SetCookie(*resp, &accessCookie)
|
||||
http.SetCookie(*resp, &refreshCookie)
|
||||
|
||||
// Do Login
|
||||
foundUser, success := r.Auth.AuthenticateUser(user, password)
|
||||
if !success {
|
||||
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
|
||||
}
|
||||
|
||||
// Upsert Device
|
||||
foundDevice := model.Device{UserID: foundUser.ID}
|
||||
if deviceID != nil {
|
||||
parsedDeviceID, err := uuid.Parse(*deviceID)
|
||||
if err != nil {
|
||||
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
|
||||
}
|
||||
foundDevice.ID = parsedDeviceID.String()
|
||||
count, err := r.DB.Device(&foundDevice)
|
||||
if count != 1 || err != nil {
|
||||
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
|
||||
}
|
||||
} else {
|
||||
foundDevice.Type = deriveDeviceType(req)
|
||||
err := r.DB.CreateDevice(&foundDevice)
|
||||
if err != nil {
|
||||
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Create Tokens
|
||||
accessToken, err := r.Auth.CreateJWTAccessToken(foundUser, foundDevice)
|
||||
if err != nil {
|
||||
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
|
||||
}
|
||||
refreshToken, err := r.Auth.CreateJWTRefreshToken(foundUser, foundDevice)
|
||||
if err != nil {
|
||||
return &model.AuthResponse{Result: model.AuthResultFailure}, nil
|
||||
}
|
||||
|
||||
// Set appropriate cookies
|
||||
accessCookie = http.Cookie{Name: "AccessToken", Value: accessToken, Path: "/", HttpOnly: true}
|
||||
refreshCookie = http.Cookie{Name: "RefreshToken", Value: refreshToken, Path: "/", HttpOnly: true}
|
||||
http.SetCookie(*resp, &accessCookie)
|
||||
http.SetCookie(*resp, &refreshCookie)
|
||||
|
||||
return &model.AuthResponse{Result: model.AuthResultSuccess, Device: &foundDevice}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Logout(ctx context.Context) (*model.AuthResponse, error) {
|
||||
// Acquire Context
|
||||
resp, _, err := getContextHTTP(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Clear All Cookies
|
||||
accessCookie := http.Cookie{Name: "AccessToken", Path: "/", HttpOnly: true, MaxAge: -1, Expires: time.Now().Add(-100 * time.Hour)}
|
||||
refreshCookie := http.Cookie{Name: "RefreshToken", Path: "/", HttpOnly: true, MaxAge: -1, Expires: time.Now().Add(-100 * time.Hour)}
|
||||
http.SetCookie(*resp, &accessCookie)
|
||||
http.SetCookie(*resp, &refreshCookie)
|
||||
|
||||
return &model.AuthResponse{Result: model.AuthResultSuccess}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MediaItem(ctx context.Context, id string) (*model.MediaItem, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mediaItemID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid ID Format")
|
||||
}
|
||||
|
||||
foundMediaItem := &model.MediaItem{ID: mediaItemID.String(), UserID: userID}
|
||||
count, err := r.DB.MediaItem(foundMediaItem)
|
||||
if err != nil {
|
||||
return nil, errors.New("DB Error")
|
||||
} else if count != 1 {
|
||||
return nil, errors.New("MediaItem Not Found")
|
||||
}
|
||||
return foundMediaItem, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Device(ctx context.Context, id string) (*model.Device, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deviceID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid ID Format")
|
||||
}
|
||||
|
||||
foundDevice := &model.Device{ID: deviceID.String(), UserID: userID}
|
||||
count, err := r.DB.Device(foundDevice)
|
||||
if err != nil {
|
||||
return nil, errors.New("DB Error")
|
||||
} else if count != 1 {
|
||||
return nil, errors.New("Device Not Found")
|
||||
}
|
||||
return foundDevice, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Album(ctx context.Context, id string) (*model.Album, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
albumID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid ID Format")
|
||||
}
|
||||
|
||||
foundAlbum := &model.Album{ID: albumID.String(), UserID: userID}
|
||||
count, err := r.DB.Album(foundAlbum)
|
||||
if err != nil {
|
||||
return nil, errors.New("DB Error")
|
||||
} else if count != 1 {
|
||||
return nil, errors.New("Album Not Found")
|
||||
}
|
||||
return foundAlbum, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
|
||||
userID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid ID Format")
|
||||
}
|
||||
|
||||
foundUser := &model.User{ID: userID.String()}
|
||||
count, err := r.DB.User(foundUser)
|
||||
if err != nil {
|
||||
return nil, errors.New("DB Error")
|
||||
} else if count != 1 {
|
||||
return nil, errors.New("User Not Found")
|
||||
}
|
||||
return foundUser, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tagID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid ID Format")
|
||||
}
|
||||
|
||||
foundTag := &model.Tag{ID: tagID.String(), UserID: userID}
|
||||
count, err := r.DB.Tag(foundTag)
|
||||
if err != nil {
|
||||
return nil, errors.New("DB Error")
|
||||
} else if count != 1 {
|
||||
return nil, errors.New("Tag Not Found")
|
||||
}
|
||||
return foundTag, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
foundUser := &model.User{ID: userID}
|
||||
count, err := r.DB.User(foundUser)
|
||||
if err != nil || count != 1 {
|
||||
return nil, errors.New("DB Error")
|
||||
}
|
||||
return foundUser, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MediaItems(ctx context.Context, filter *model.MediaItemFilter, page *model.Page, order *model.Order) (*model.MediaItemResponse, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, pageResponse, err := r.DB.MediaItems(userID, filter, page, order)
|
||||
if err != nil {
|
||||
return nil, errors.New("DB Error")
|
||||
}
|
||||
return &model.MediaItemResponse{
|
||||
Data: resp,
|
||||
Page: &pageResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Devices(ctx context.Context, filter *model.DeviceFilter, page *model.Page, order *model.Order) (*model.DeviceResponse, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, pageResponse, err := r.DB.Devices(userID, filter, page, order)
|
||||
if err != nil {
|
||||
return nil, errors.New("DB Error")
|
||||
}
|
||||
return &model.DeviceResponse{
|
||||
Data: resp,
|
||||
Page: &pageResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Albums(ctx context.Context, filter *model.AlbumFilter, page *model.Page, order *model.Order) (*model.AlbumResponse, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, pageResponse, err := r.DB.Albums(userID, filter, page, order)
|
||||
if err != nil {
|
||||
return nil, errors.New("Context Error")
|
||||
}
|
||||
return &model.AlbumResponse{
|
||||
Data: resp,
|
||||
Page: &pageResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Tags(ctx context.Context, filter *model.TagFilter, page *model.Page, order *model.Order) (*model.TagResponse, error) {
|
||||
// Acquire Context
|
||||
userID, _, err := getContextIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, pageResponse, err := r.DB.Tags(userID, filter, page, order)
|
||||
if err != nil {
|
||||
return nil, errors.New("Context Error")
|
||||
}
|
||||
return &model.TagResponse{
|
||||
Data: resp,
|
||||
Page: &pageResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Users(ctx context.Context, filter *model.UserFilter, page *model.Page, order *model.Order) (*model.UserResponse, error) {
|
||||
resp, pageResponse, err := r.DB.Users(filter, page, order)
|
||||
if err != nil {
|
||||
return nil, errors.New("Context Error")
|
||||
}
|
||||
return &model.UserResponse{
|
||||
Data: resp,
|
||||
Page: &pageResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Mutation returns generated.MutationResolver implementation.
|
||||
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
// Query returns generated.QueryResolver implementation.
|
||||
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
@ -1,9 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) albumsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
@ -1,227 +1,94 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"strings"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
"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 models.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)
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* This will find or create the requested device based on ID and User.
|
||||
**/
|
||||
func (api *API) upsertRequestedDevice(user models.User, r *http.Request) (models.Device, error) {
|
||||
requestedDevice := deriveRequestedDevice(r)
|
||||
requestedDevice.Type = deriveDeviceType(r)
|
||||
requestedDevice.UserUUID = user.UUID
|
||||
|
||||
if requestedDevice.UUID == uuid.Nil {
|
||||
err := api.DB.CreateDevice(&requestedDevice)
|
||||
createdDevice, err := api.DB.Device(&requestedDevice)
|
||||
return createdDevice, err
|
||||
}
|
||||
|
||||
foundDevice, err := api.DB.Device(&models.Device{
|
||||
Base: models.Base{ UUID: requestedDevice.UUID },
|
||||
User: user,
|
||||
})
|
||||
|
||||
return foundDevice, err
|
||||
}
|
||||
|
||||
func deriveDeviceType(r *http.Request) string {
|
||||
userAgent := strings.ToLower(r.Header.Get("User-Agent"))
|
||||
if strings.HasPrefix(userAgent, "ios-imagini"){
|
||||
return "iOS"
|
||||
} else if strings.HasPrefix(userAgent, "android-imagini"){
|
||||
return "Android"
|
||||
} else if strings.HasPrefix(userAgent, "chrome"){
|
||||
return "Chrome"
|
||||
} else if strings.HasPrefix(userAgent, "firefox"){
|
||||
return "Firefox"
|
||||
} else if strings.HasPrefix(userAgent, "msie"){
|
||||
return "Internet Explorer"
|
||||
} else if strings.HasPrefix(userAgent, "edge"){
|
||||
return "Edge"
|
||||
} else if strings.HasPrefix(userAgent, "safari"){
|
||||
return "Safari"
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
func deriveRequestedDevice(r *http.Request) models.Device {
|
||||
deviceSkeleton := models.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 != "deviceuuid" && key != "devicename" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Derive Value
|
||||
val := trimQuotes(strings.TrimSpace(splitItem[1]))
|
||||
if key == "deviceuuid" {
|
||||
parsedDeviceUUID, err := uuid.Parse(val)
|
||||
if err != nil {
|
||||
log.Warn("[auth] deriveRequestedDevice - Unable to parse requested DeviceUUID: ", val)
|
||||
continue
|
||||
}
|
||||
deviceSkeleton.Base = models.Base{UUID: parsedDeviceUUID}
|
||||
} else if key == "devicename" {
|
||||
deviceSkeleton.Name = val
|
||||
}
|
||||
}
|
||||
|
||||
// If name not set, set to type
|
||||
if deviceSkeleton.Name == "" {
|
||||
deviceSkeleton.Name = deviceSkeleton.Type
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Device & User Skeleton
|
||||
user := models.User{Base: models.Base{UUID: userUUID}}
|
||||
device := models.Device{Base: models.Base{UUID: deviceUUID}}
|
||||
|
||||
// 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
|
||||
// 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))
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) devicesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
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")
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) infoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
@ -1,50 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
// Responsible for serving up static images / videos
|
||||
/**
|
||||
* Responsible for serving up static images / videos
|
||||
**/
|
||||
func (api *API) mediaHandler(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
}
|
||||
|
1
internal/api/media_item.go
Normal file
1
internal/api/media_item.go
Normal file
@ -0,0 +1 @@
|
||||
package api
|
@ -1,327 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
"regexp"
|
||||
"strings"
|
||||
"errors"
|
||||
"net/url"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/dsoprea/go-exif/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/dsoprea/go-exif/v3/common"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
)
|
||||
|
||||
// GET
|
||||
// - /api/v1/MediaItems/<GUID>
|
||||
// - JSON Struct
|
||||
// - /api/v1/MediaItems/<GUID>/content
|
||||
// - The raw file
|
||||
func (api *API) mediaItemsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
// CREATE
|
||||
api.mediaItemPOSTHandler(w, r)
|
||||
} else if r.Method == http.MethodPut {
|
||||
// UPDATE / REPLACE
|
||||
} else if r.Method == http.MethodPatch {
|
||||
// UPDATE / MODIFY
|
||||
} else if r.Method == http.MethodDelete {
|
||||
// DELETE
|
||||
} else if r.Method == http.MethodGet {
|
||||
// GET
|
||||
api.mediaItemGETHandler(w, r)
|
||||
} else {
|
||||
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
func (api *API) mediaItemGETHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
testObj := models.MediaItem{}
|
||||
json.NewDecoder().Decode(&testObj)
|
||||
fmt.Printf("Result: %+v\n", testObj)
|
||||
|
||||
// allParams, err := json.Marshal(r.Form)
|
||||
// if err != nil {
|
||||
// // Handle error
|
||||
// }
|
||||
|
||||
// filter := &models.MediaItem{}
|
||||
// if err = json.Unmarshal(allParams, filter); err != nil {
|
||||
// // Handle error
|
||||
// fmt.Printf("Fuck: %s\n", err)
|
||||
// }
|
||||
|
||||
// fmt.Printf("Result: %+v\n", filter)
|
||||
|
||||
// err = normalizeForm(r.Form, models.MediaItem{})
|
||||
// if err != nil {
|
||||
// fmt.Printf("Error: %s\n", err)
|
||||
// }
|
||||
|
||||
// var testItems []models.MediaItem
|
||||
// api.DB.QueryBuilder(&testItems, allParams)
|
||||
|
||||
// fmt.Printf("\n\nItems: %+v", testItems)
|
||||
|
||||
|
||||
// Pull out UUIDs
|
||||
reqInfo := r.Context().Value("uuids").(map[string]string)
|
||||
uid := reqInfo["uid"]
|
||||
userUUID, _ := uuid.Parse(uid)
|
||||
|
||||
// TODO: Can apply multiple filters based on query parameters
|
||||
mediaItemFilter := &models.MediaItem{UserUUID: userUUID}
|
||||
mediaItemFilter.UserUUID = userUUID
|
||||
|
||||
mediaItems, count, _ := api.DB.MediaItems(mediaItemFilter)
|
||||
response := &models.APIResponse{
|
||||
Data: &mediaItems,
|
||||
Meta: &models.APIMeta{Count: count},
|
||||
}
|
||||
responseJSON(w, &response, http.StatusOK)
|
||||
}
|
||||
|
||||
func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 64MB limit (TODO: Change this - video)
|
||||
r.ParseMultipartForm(64 << 20)
|
||||
|
||||
// Open form file
|
||||
formFile, multipartFileHeader, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer formFile.Close()
|
||||
|
||||
// File header placeholder
|
||||
fileHeader := make([]byte, 64)
|
||||
|
||||
// Copy headers into the buffer
|
||||
if _, err := formFile.Read(fileHeader); err != nil {
|
||||
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Reset position
|
||||
if _, err := formFile.Seek(0, 0); err != nil {
|
||||
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine media type
|
||||
fileMime := mimetype.Detect(fileHeader)
|
||||
contentType := fileMime.String()
|
||||
var mediaType string
|
||||
if strings.HasPrefix(contentType, "image/") {
|
||||
mediaType = "Image"
|
||||
} else if strings.HasPrefix(contentType, "video/") {
|
||||
mediaType = "Video"
|
||||
} else {
|
||||
errorJSON(w, "Invalid filetype.", http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
||||
// Pull out UUIDs
|
||||
reqInfo := r.Context().Value("uuids").(map[string]string)
|
||||
uid := reqInfo["uid"]
|
||||
|
||||
// Derive Folder & File Path
|
||||
mediaItemUUID := uuid.New()
|
||||
fileName := mediaItemUUID.String() + fileMime.Extension()
|
||||
folderPath := path.Join("/" + api.Config.DataPath + "/media/" + uid)
|
||||
os.MkdirAll(folderPath, 0700)
|
||||
filePath := path.Join(folderPath + "/" + fileName)
|
||||
|
||||
// Create File
|
||||
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
log.Warn("[api] createMediaItem - Unable to open file: ", filePath)
|
||||
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Copy data to file
|
||||
_, err = io.Copy(f, formFile)
|
||||
if err != nil {
|
||||
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create MediaItem From EXIF Data
|
||||
mediaItem, err := mediaItemFromEXIFData(filePath)
|
||||
if err != nil {
|
||||
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Add Additional MediaItem Fields
|
||||
mediaItem.Base.UUID = mediaItemUUID
|
||||
mediaItem.UserUUID, err = uuid.Parse(uid)
|
||||
mediaItem.MediaType = mediaType
|
||||
mediaItem.FileName = fileName
|
||||
mediaItem.OrigName = multipartFileHeader.Filename
|
||||
|
||||
// Create MediaItem in DB
|
||||
err = api.DB.CreateMediaItem(mediaItem)
|
||||
if err != nil {
|
||||
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
successJSON(w, "Upload succeeded.", http.StatusCreated)
|
||||
}
|
||||
|
||||
func mediaItemFromEXIFData(filePath string) (*models.MediaItem, error) {
|
||||
rawExif, err := exif.SearchFileAndExtractExif(filePath)
|
||||
entries, _, err := exif.GetFlatExifData(rawExif, nil)
|
||||
|
||||
decLong := float32(1)
|
||||
decLat := float32(1)
|
||||
|
||||
mediaItem := &models.MediaItem{}
|
||||
for _, v := range entries {
|
||||
if v.TagName == "DateTimeOriginal" {
|
||||
formattedTime, _ := time.Parse("2006:01:02 15:04:05", v.Formatted)
|
||||
mediaItem.EXIFDate = formattedTime
|
||||
} else if v.TagName == "GPSLatitude" {
|
||||
latStruct := v.Value.([]exifcommon.Rational)
|
||||
decLat *= deriveDecimalCoordinate(
|
||||
latStruct[0].Numerator / latStruct[0].Denominator,
|
||||
latStruct[1].Numerator / latStruct[1].Denominator,
|
||||
float32(latStruct[2].Numerator) / float32(latStruct[2].Denominator),
|
||||
)
|
||||
} else if v.TagName == "GPSLongitude" {
|
||||
longStruct := v.Value.([]exifcommon.Rational)
|
||||
decLong *= deriveDecimalCoordinate(
|
||||
longStruct[0].Numerator / longStruct[0].Denominator,
|
||||
longStruct[1].Numerator / longStruct[1].Denominator,
|
||||
float32(longStruct[2].Numerator) / float32(longStruct[2].Denominator),
|
||||
)
|
||||
} else if v.TagName == "GPSLatitudeRef" && v.Formatted == "S" {
|
||||
decLat *= -1
|
||||
} else if v.TagName == "GPSLongitudeRef" && v.Formatted == "W" {
|
||||
decLong *= -1
|
||||
}
|
||||
}
|
||||
|
||||
mediaItem.Latitude = &decLat
|
||||
mediaItem.Longitude = &decLong
|
||||
|
||||
return mediaItem, err
|
||||
}
|
||||
|
||||
func deriveDecimalCoordinate(degrees, minutes uint32, seconds float32) float32 {
|
||||
return float32(degrees) + (float32(minutes) / 60) + (seconds / 3600)
|
||||
}
|
||||
|
||||
// {
|
||||
// filters: [
|
||||
// { field: "", operator: ""},
|
||||
// { field: "", operator: ""},
|
||||
// { field: "", operator: ""},
|
||||
// ],
|
||||
// sort: ""
|
||||
// page: {}
|
||||
// }
|
||||
func normalizeForm(form url.Values, typeObj interface{}) error {
|
||||
allowedFields := models.JSONFields(typeObj)
|
||||
|
||||
for key, val := range form {
|
||||
key = strings.ToLower(key)
|
||||
|
||||
re := regexp.MustCompile(`^(filter|page)\[(\w*)]($|>|<)$`)
|
||||
matches := re.FindStringSubmatch(key)
|
||||
|
||||
if len(matches) == 4 {
|
||||
cmd := strings.ToLower(matches[1])
|
||||
field := strings.ToLower(matches[2])
|
||||
operator := strings.ToLower(matches[3])
|
||||
|
||||
if cmd == "page" && field == "limit" {
|
||||
fmt.Printf("cmd: %s field: %s op: %s\n", cmd, field, operator)
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate field
|
||||
_, ok := allowedFields[field]
|
||||
if !ok {
|
||||
return errors.New("Invalid field.")
|
||||
}
|
||||
|
||||
// Val assertions
|
||||
tempObj := make(map[string]string)
|
||||
tempObj[field] = val[0]
|
||||
|
||||
mi, err := json.Marshal(tempObj)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
fmt.Printf("1 Type Assertion Failed For Field: [%s] with value: [%s]\n", field, val)
|
||||
}
|
||||
fmt.Printf("String JSON: %s", string(mi))
|
||||
refObj := &models.MediaItem{}
|
||||
if err = json.Unmarshal(mi, refObj); err != nil {
|
||||
// Handle error
|
||||
fmt.Printf("2 Type Assertion Failed For Field: [%s] with value: [%s]\n", field, val[0])
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Result: %+v\n", refObj)
|
||||
|
||||
fmt.Printf("cmd: %s field: %s op: %s\n", cmd, field, operator)
|
||||
} else if key == "sort" {
|
||||
field := strings.ToLower(val[0])
|
||||
|
||||
// Validate field
|
||||
_, ok := allowedFields[field]
|
||||
if !ok {
|
||||
return errors.New("Invalid field.")
|
||||
}
|
||||
|
||||
// TODO: Validate val
|
||||
|
||||
fmt.Printf("cmd: %s\n", key)
|
||||
} else {
|
||||
return errors.New("Invalid parameter(s)")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,73 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"context"
|
||||
"net/http"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"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
|
||||
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) contextMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
// Acquire Token
|
||||
accessCookie, err := r.Cookie("AccessToken")
|
||||
if err != nil {
|
||||
log.Warn("[middleware] AccessToken not found")
|
||||
errorJSON(w, "Invalid token.", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// Create Context
|
||||
authContext := &model.AuthContext{
|
||||
AccessToken: &accessToken,
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), "auth", authContext)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// Validate JWT Tokens
|
||||
accessToken, err := api.Auth.ValidateJWTAccessToken(accessCookie.Value)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
@ -1,72 +1,36 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph"
|
||||
"reichard.io/imagini/graph/generated"
|
||||
)
|
||||
|
||||
func (api *API) registerRoutes() {
|
||||
api.Router.HandleFunc("/media/", multipleMiddleware(
|
||||
api.mediaHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
api.Router.HandleFunc("/api/v1/MediaItems", multipleMiddleware(
|
||||
api.mediaItemsHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
api.Router.HandleFunc("/api/v1/Devices", multipleMiddleware(
|
||||
api.devicesHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
api.Router.HandleFunc("/api/v1/Upload", multipleMiddleware(
|
||||
api.uploadHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
api.Router.HandleFunc("/api/v1/Albums", multipleMiddleware(
|
||||
api.albumsHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
api.Router.HandleFunc("/api/v1/Users", multipleMiddleware(
|
||||
api.usersHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
api.Router.HandleFunc("/api/v1/Tags", multipleMiddleware(
|
||||
api.tagsHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
api.Router.HandleFunc("/api/v1/Info", multipleMiddleware(
|
||||
api.infoHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
api.Router.HandleFunc("/api/v1/Me", multipleMiddleware(
|
||||
api.meHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
// 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))
|
||||
|
||||
api.Router.HandleFunc("/api/v1/Logout", api.logoutHandler)
|
||||
api.Router.HandleFunc("/api/v1/Login", api.loginHandler)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// https://stackoverflow.com/a/59764037
|
||||
func errorJSON(w http.ResponseWriter, err string, code int) {
|
||||
errStruct := &models.APIResponse{Error: &models.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})
|
||||
// Handle GraphQL
|
||||
api.Router.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
|
||||
api.Router.Handle("/query", api.contextMiddleware(srv))
|
||||
|
||||
// Handle Resource Route
|
||||
api.Router.HandleFunc("/media/", multipleMiddleware(
|
||||
api.mediaHandler,
|
||||
api.authMiddleware,
|
||||
))
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) tagsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) uploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
// log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (api *API) usersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
// CREATE
|
||||
} else if r.Method == http.MethodPut {
|
||||
// UPDATE / REPLACE
|
||||
} else if r.Method == http.MethodPatch {
|
||||
// UPDATE / MODIFY
|
||||
} else if r.Method == http.MethodDelete {
|
||||
// DELETE
|
||||
} else if r.Method == http.MethodGet {
|
||||
// GET
|
||||
} else {
|
||||
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) meHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
}
|
@ -1,179 +1,57 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/lestrrat-go/jwx/jwa"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"reichard.io/imagini/internal/db"
|
||||
"reichard.io/imagini/internal/config"
|
||||
"reichard.io/imagini/internal/models"
|
||||
"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(creds models.APICredentials) (bool, models.User) {
|
||||
// By Username
|
||||
foundUser, err := auth.DB.User(&models.User{Username: creds.User})
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
foundUser, err = auth.DB.User(&models.User{Email: creds.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)
|
||||
|
||||
// Error Checking
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Warn("[auth] User not found: ", creds.User)
|
||||
return false, foundUser
|
||||
} else if err != nil {
|
||||
log.Error(err)
|
||||
return false, foundUser
|
||||
}
|
||||
// By Username
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
foundUser = &model.User{Email: user}
|
||||
_, err = auth.DB.User(foundUser)
|
||||
}
|
||||
|
||||
log.Info("[auth] Authenticating user: ", foundUser.Username)
|
||||
// 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
|
||||
}
|
||||
|
||||
// Determine Type
|
||||
switch foundUser.AuthType {
|
||||
case "Local":
|
||||
return authenticateLocalUser(foundUser, creds.Password), foundUser
|
||||
case "LDAP":
|
||||
return authenticateLDAPUser(foundUser, creds.Password), foundUser
|
||||
default:
|
||||
return false, foundUser
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *AuthManager) getRole(user models.User) string {
|
||||
// TODO: Lookup role of user
|
||||
return "User"
|
||||
}
|
||||
|
||||
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, err := auth.DB.Device(&models.Device{Base: models.Base{UUID: deviceID}})
|
||||
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 models.User, device models.Device) (string, error) {
|
||||
// Acquire Refresh Key
|
||||
byteKey := []byte(device.RefreshKey)
|
||||
|
||||
// Create New Token
|
||||
tm := time.Now()
|
||||
t := jwt.New()
|
||||
t.Set(`did`, device.UUID.String()) // Device ID
|
||||
t.Set(jwt.SubjectKey, user.UUID.String()) // 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 models.User, device models.Device) (string, error) {
|
||||
// Create New Token
|
||||
tm := time.Now()
|
||||
t := jwt.New()
|
||||
t.Set(`did`, device.UUID.String()) // Device ID
|
||||
t.Set(`role`, auth.getRole(user)) // User Role (Admin / User)
|
||||
t.Set(jwt.SubjectKey, user.UUID.String()) // 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
|
||||
log.Info("[auth] Authenticating user: ", foundUser.Username)
|
||||
|
||||
// Determine Type
|
||||
switch foundUser.AuthType {
|
||||
case "Local":
|
||||
return *foundUser, authenticateLocalUser(*foundUser, password)
|
||||
case "LDAP":
|
||||
return *foundUser, authenticateLDAPUser(*foundUser, password)
|
||||
default:
|
||||
return *foundUser, false
|
||||
}
|
||||
}
|
||||
|
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,9 +1,9 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func authenticateLDAPUser(user models.User, pw string) bool {
|
||||
return false
|
||||
func authenticateLDAPUser(user model.User, pw string) bool {
|
||||
return false
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/imagini/internal/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func authenticateLocalUser(user models.User, pw string) bool {
|
||||
bPassword :=[]byte(pw)
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password), bPassword)
|
||||
if err == nil {
|
||||
log.Info("[auth] Authentication successfull: ", user.Username)
|
||||
return true
|
||||
}
|
||||
log.Warn("[auth] Authentication failed: ", user.Username)
|
||||
return false
|
||||
func authenticateLocalUser(user model.User, pw string) bool {
|
||||
bPassword := []byte(pw)
|
||||
err := bcrypt.CompareHashAndPassword([]byte(*user.Password), bPassword)
|
||||
if err == nil {
|
||||
log.Info("[auth] Authentication successfull: ", user.Username)
|
||||
return true
|
||||
}
|
||||
log.Warn("[auth] Authentication failed: ", user.Username)
|
||||
return false
|
||||
}
|
||||
|
@ -1,34 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DBType string
|
||||
DBName string
|
||||
DBPassword string
|
||||
DataPath string
|
||||
ConfigPath string
|
||||
JWTSecret string
|
||||
ListenPort string
|
||||
DBType string
|
||||
DBName string
|
||||
DBPassword string
|
||||
DataPath string
|
||||
ConfigPath string
|
||||
JWTSecret string
|
||||
ListenPort string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
DBType: getEnv("DATABASE_TYPE", "SQLite"),
|
||||
DBName: getEnv("DATABASE_NAME", "imagini"),
|
||||
DBPassword: getEnv("DATABASE_PASSWORD", ""),
|
||||
ConfigPath: getEnv("CONFIG_PATH", "/config"),
|
||||
DataPath: getEnv("DATA_PATH", "/data"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "58b9340c0472cf045db226bc445966524e780cd38bc3dd707afce80c95d4de6f"),
|
||||
ListenPort: getEnv("LISTEN_PORT", "8484"),
|
||||
}
|
||||
return &Config{
|
||||
DBType: getEnv("DATABASE_TYPE", "SQLite"),
|
||||
DBName: getEnv("DATABASE_NAME", "imagini"),
|
||||
DBPassword: getEnv("DATABASE_PASSWORD", ""),
|
||||
ConfigPath: getEnv("CONFIG_PATH", "/config"),
|
||||
DataPath: getEnv("DATA_PATH", "/data"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "58b9340c0472cf045db226bc445966524e780cd38bc3dd707afce80c95d4de6f"),
|
||||
ListenPort: getEnv("LISTEN_PORT", "8484"),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
@ -1 +1,37 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (dbm *DBManager) CreateAlbum(album *model.Album) error {
|
||||
log.Debug("[db] Creating album: ", album.Name)
|
||||
err := dbm.db.Create(album).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) Album(album *model.Album) (int64, error) {
|
||||
var count int64
|
||||
err := dbm.db.Where(album).First(album).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) Albums(userID string, filters *model.AlbumFilter, page *model.Page, order *model.Order) ([]*model.Album, model.PageResponse, error) {
|
||||
// Initial User Filter
|
||||
tx := dbm.db.Session(&gorm.Session{}).Model(&model.Album{}).Where("user_id == ?", userID)
|
||||
|
||||
// Dynamically Generate Base Query
|
||||
tx, pageResponse := dbm.generateBaseQuery(tx, filters, page, order)
|
||||
|
||||
// Acquire Results
|
||||
var foundAlbums []*model.Album
|
||||
err := tx.Find(&foundAlbums).Error
|
||||
return foundAlbums, pageResponse, err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) DeleteAlbum(album *model.Album) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -1,109 +1,155 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"reflect"
|
||||
|
||||
"gorm.io/gorm"
|
||||
// "gorm.io/gorm/logger"
|
||||
"gorm.io/driver/sqlite"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/iancoleman/strcase"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"reichard.io/imagini/internal/config"
|
||||
"reichard.io/imagini/internal/models"
|
||||
// "gorm.io/gorm/logger"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"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)
|
||||
dbm.db = dbm.db.Debug()
|
||||
} else {
|
||||
log.Fatal("Unsupported Database")
|
||||
}
|
||||
|
||||
// Initialize database
|
||||
dbm.db.AutoMigrate(&models.ServerSetting{})
|
||||
dbm.db.AutoMigrate(&models.Device{})
|
||||
dbm.db.AutoMigrate(&models.User{})
|
||||
dbm.db.AutoMigrate(&models.MediaItem{})
|
||||
dbm.db.AutoMigrate(&models.Tag{})
|
||||
dbm.db.AutoMigrate(&models.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(&models.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.")
|
||||
err := dbm.CreateUser(&models.User{
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
AuthType: "Local",
|
||||
})
|
||||
log.Info("[query] Bootstrapping database.")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("[query] Unable to bootstrap database.")
|
||||
}
|
||||
password := "admin"
|
||||
user := &model.User{
|
||||
Username: "admin",
|
||||
AuthType: "Local",
|
||||
Password: &password,
|
||||
Role: model.RoleAdmin,
|
||||
}
|
||||
|
||||
err := dbm.CreateUser(user)
|
||||
|
||||
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
|
||||
|
||||
objType := fmt.Sprintf("%T", dest)
|
||||
if objType == "*[]models.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
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
func (dbm *DBManager) generateBaseQuery(tx *gorm.DB, filter interface{}, page *model.Page, order *model.Order) (*gorm.DB, model.PageResponse) {
|
||||
tx = dbm.generateFilter(tx, filter)
|
||||
tx = dbm.generateOrder(tx, order, filter)
|
||||
tx, pageResponse := dbm.generatePage(tx, page)
|
||||
return tx, pageResponse
|
||||
}
|
||||
|
||||
func (dbm *DBManager) generateOrder(tx *gorm.DB, order *model.Order, filter interface{}) *gorm.DB {
|
||||
// Set Defaults
|
||||
orderBy := "created_at"
|
||||
orderDirection := model.OrderDirectionDesc
|
||||
|
||||
if order == nil {
|
||||
order = &model.Order{
|
||||
By: &orderBy,
|
||||
Direction: &orderDirection,
|
||||
}
|
||||
}
|
||||
|
||||
if order.By == nil {
|
||||
order.By = &orderBy
|
||||
}
|
||||
|
||||
if order.Direction == nil {
|
||||
order.Direction = &orderDirection
|
||||
}
|
||||
|
||||
// Get Possible Values
|
||||
ptr := reflect.New(reflect.TypeOf(filter).Elem())
|
||||
v := reflect.Indirect(ptr)
|
||||
|
||||
isValid := false
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fieldName := v.Type().Field(i).Name
|
||||
if strcase.ToSnake(*order.By) == strcase.ToSnake(fieldName) {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isValid {
|
||||
tx = tx.Order(fmt.Sprintf("%s %s", strcase.ToSnake(*order.By), order.Direction.String()))
|
||||
}
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
func (dbm *DBManager) generatePage(tx *gorm.DB, page *model.Page) (*gorm.DB, model.PageResponse) {
|
||||
// Set Defaults
|
||||
var count int64
|
||||
pageSize := 50
|
||||
pageNum := 1
|
||||
|
||||
if page == nil {
|
||||
page = &model.Page{
|
||||
Size: &pageSize,
|
||||
Page: &pageNum,
|
||||
}
|
||||
}
|
||||
|
||||
if page.Size == nil {
|
||||
page.Size = &pageSize
|
||||
}
|
||||
|
||||
if page.Page == nil {
|
||||
page.Page = &pageNum
|
||||
}
|
||||
|
||||
// Acquire Counts Before Pagination
|
||||
tx.Count(&count)
|
||||
|
||||
// Calculate Offset
|
||||
calculatedOffset := (*page.Page - 1) * *page.Size
|
||||
tx = tx.Limit(*page.Size).Offset(calculatedOffset)
|
||||
|
||||
return tx, model.PageResponse{
|
||||
Page: *page.Page,
|
||||
Size: *page.Size,
|
||||
Total: int(count),
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,44 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (dbm *DBManager) CreateDevice (device *models.Device) error {
|
||||
log.Info("[db] Creating device: ", device.Name)
|
||||
device.RefreshKey = uuid.New().String()
|
||||
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 *models.Device) (models.Device, error) {
|
||||
var foundDevice models.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 *models.Device) error {
|
||||
return nil
|
||||
func (dbm *DBManager) Devices(userID string, filters *model.DeviceFilter, page *model.Page, order *model.Order) ([]*model.Device, model.PageResponse, error) {
|
||||
// Initial User Filter
|
||||
tx := dbm.db.Session(&gorm.Session{}).Model(&model.Device{}).Where("user_id == ?", userID)
|
||||
|
||||
// Dynamically Generate Base Query
|
||||
tx, pageResponse := dbm.generateBaseQuery(tx, filters, page, order)
|
||||
|
||||
// Acquire Results
|
||||
var foundDevices []*model.Device
|
||||
err := tx.Find(&foundDevices).Error
|
||||
return foundDevices, pageResponse, err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) UpdateRefreshToken (device *models.Device, refreshToken string) 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
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package db
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
)
|
196
internal/db/filters.go
Normal file
196
internal/db/filters.go
Normal file
@ -0,0 +1,196 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
"gorm.io/gorm"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
// Generic function used to generate filters for the DB
|
||||
func (dbm *DBManager) generateFilter(tx *gorm.DB, filter interface{}) *gorm.DB {
|
||||
ptr := reflect.ValueOf(filter)
|
||||
v := reflect.Indirect(ptr)
|
||||
|
||||
if v == reflect.ValueOf(nil) {
|
||||
return tx
|
||||
}
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fieldName := strcase.ToSnake(v.Type().Field(i).Name)
|
||||
fieldVal := v.Field(i)
|
||||
|
||||
if fieldVal.IsNil() {
|
||||
continue
|
||||
}
|
||||
|
||||
switch valType := fieldVal.Type(); valType {
|
||||
case reflect.TypeOf(&model.StringFilter{}):
|
||||
tx = generateStringFilter(tx, fieldName, fieldVal.Interface().(*model.StringFilter))
|
||||
case reflect.TypeOf(&model.BooleanFilter{}):
|
||||
tx = generateBooleanFilter(tx, fieldName, fieldVal.Interface().(*model.BooleanFilter))
|
||||
case reflect.TypeOf(&model.FloatFilter{}):
|
||||
tx = generateFloatFilter(tx, fieldName, fieldVal.Interface().(*model.FloatFilter))
|
||||
case reflect.TypeOf(&model.IntFilter{}):
|
||||
tx = generateIntFilter(tx, fieldName, fieldVal.Interface().(*model.IntFilter))
|
||||
case reflect.TypeOf(&model.IDFilter{}):
|
||||
tx = generateIDFilter(tx, fieldName, fieldVal.Interface().(*model.IDFilter))
|
||||
case reflect.TypeOf(&model.TimeFilter{}):
|
||||
tx = generateTimeFilter(tx, fieldName, fieldVal.Interface().(*model.TimeFilter))
|
||||
case reflect.TypeOf(&model.RoleFilter{}):
|
||||
tx = generateRoleFilter(tx, fieldName, fieldVal.Interface().(*model.RoleFilter))
|
||||
case reflect.TypeOf(&model.DeviceTypeFilter{}):
|
||||
tx = generateDeviceTypeFilter(tx, fieldName, fieldVal.Interface().(*model.DeviceTypeFilter))
|
||||
case reflect.TypeOf(&model.AuthTypeFilter{}):
|
||||
tx = generateAuthTypeFilter(tx, fieldName, fieldVal.Interface().(*model.AuthTypeFilter))
|
||||
}
|
||||
}
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
func generateStringFilter(tx *gorm.DB, fieldName string, filter *model.StringFilter) *gorm.DB {
|
||||
if filter.EqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s == ?", fieldName), *filter.EqualTo)
|
||||
}
|
||||
if filter.NotEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s != ?", fieldName), *filter.NotEqualTo)
|
||||
}
|
||||
if filter.StartsWith != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s LIKE ?", fieldName), fmt.Sprintf("%s%%", *filter.StartsWith))
|
||||
}
|
||||
if filter.NotStartsWith != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s NOT LIKE ?", fieldName), fmt.Sprintf("%s%%", *filter.NotStartsWith))
|
||||
}
|
||||
if filter.EndsWith != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s LIKE ?", fieldName), fmt.Sprintf("%%%s", *filter.EndsWith))
|
||||
}
|
||||
if filter.NotEndsWith != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s NOT LIKE ?", fieldName), fmt.Sprintf("%%%s", *filter.NotEndsWith))
|
||||
}
|
||||
if filter.Contains != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s LIKE ?", fieldName), fmt.Sprintf("%%%s%%", *filter.Contains))
|
||||
}
|
||||
if filter.NotContains != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s NOT LIKE ?", fieldName), fmt.Sprintf("%%%s%%", *filter.NotContains))
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func generateBooleanFilter(tx *gorm.DB, fieldName string, filter *model.BooleanFilter) *gorm.DB {
|
||||
if filter.EqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s == ?", fieldName), *filter.EqualTo)
|
||||
}
|
||||
if filter.NotEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s != ?", fieldName), *filter.NotEqualTo)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func generateFloatFilter(tx *gorm.DB, fieldName string, filter *model.FloatFilter) *gorm.DB {
|
||||
if filter.EqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s == ?", fieldName), *filter.EqualTo)
|
||||
}
|
||||
if filter.NotEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s != ?", fieldName), *filter.NotEqualTo)
|
||||
}
|
||||
if filter.GreaterThan != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s > ?", fieldName), *filter.GreaterThan)
|
||||
}
|
||||
if filter.GreaterThanOrEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s >= ?", fieldName), *filter.GreaterThanOrEqualTo)
|
||||
}
|
||||
if filter.LessThan != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s < ?", fieldName), *filter.LessThan)
|
||||
}
|
||||
if filter.LessThanOrEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s <= ?", fieldName), *filter.LessThanOrEqualTo)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func generateIntFilter(tx *gorm.DB, fieldName string, filter *model.IntFilter) *gorm.DB {
|
||||
if filter.EqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s == ?", fieldName), *filter.EqualTo)
|
||||
}
|
||||
if filter.NotEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s != ?", fieldName), *filter.NotEqualTo)
|
||||
}
|
||||
if filter.GreaterThan != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s > ?", fieldName), *filter.GreaterThan)
|
||||
}
|
||||
if filter.GreaterThanOrEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s >= ?", fieldName), *filter.GreaterThanOrEqualTo)
|
||||
}
|
||||
if filter.LessThan != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s < ?", fieldName), *filter.LessThan)
|
||||
}
|
||||
if filter.LessThanOrEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s <= ?", fieldName), *filter.LessThanOrEqualTo)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func generateIDFilter(tx *gorm.DB, fieldName string, filter *model.IDFilter) *gorm.DB {
|
||||
if filter.EqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s == ?", fieldName), *filter.EqualTo)
|
||||
}
|
||||
if filter.NotEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s != ?", fieldName), *filter.NotEqualTo)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func generateTimeFilter(tx *gorm.DB, fieldName string, filter *model.TimeFilter) *gorm.DB {
|
||||
if filter.EqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s == ?", fieldName), *filter.EqualTo)
|
||||
}
|
||||
if filter.NotEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s != ?", fieldName), *filter.NotEqualTo)
|
||||
}
|
||||
if filter.GreaterThan != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s > ?", fieldName), *filter.GreaterThan)
|
||||
}
|
||||
if filter.GreaterThanOrEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s >= ?", fieldName), *filter.GreaterThanOrEqualTo)
|
||||
}
|
||||
if filter.LessThan != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s < ?", fieldName), *filter.LessThan)
|
||||
}
|
||||
if filter.LessThanOrEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s <= ?", fieldName), *filter.LessThanOrEqualTo)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func generateRoleFilter(tx *gorm.DB, fieldName string, filter *model.RoleFilter) *gorm.DB {
|
||||
if filter.EqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s == ?", fieldName), *filter.EqualTo)
|
||||
}
|
||||
if filter.NotEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s != ?", fieldName), *filter.NotEqualTo)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func generateDeviceTypeFilter(tx *gorm.DB, fieldName string, filter *model.DeviceTypeFilter) *gorm.DB {
|
||||
if filter.EqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s == ?", fieldName), *filter.EqualTo)
|
||||
}
|
||||
if filter.NotEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s != ?", fieldName), *filter.NotEqualTo)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func generateAuthTypeFilter(tx *gorm.DB, fieldName string, filter *model.AuthTypeFilter) *gorm.DB {
|
||||
if filter.EqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s == ?", fieldName), *filter.EqualTo)
|
||||
}
|
||||
if filter.NotEqualTo != nil {
|
||||
tx = tx.Where(fmt.Sprintf("%s != ?", fieldName), *filter.NotEqualTo)
|
||||
}
|
||||
return tx
|
||||
}
|
@ -1,21 +1,34 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (dbm *DBManager) CreateMediaItem (mediaItem *models.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 *models.MediaItem) ([]models.MediaItem, int64, error) {
|
||||
var mediaItems []models.MediaItem
|
||||
var count int64
|
||||
|
||||
err := dbm.db.Where(&mediaItemFilter).Find(&mediaItems).Count(&count).Error;
|
||||
return mediaItems, count, err
|
||||
func (dbm *DBManager) MediaItem(mediaItem *model.MediaItem) (int64, error) {
|
||||
var count int64
|
||||
err := dbm.db.Where(mediaItem).First(mediaItem).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// UserID, Filters, Sort, Page, Delete
|
||||
func (dbm *DBManager) MediaItems(userID string, filters *model.MediaItemFilter, page *model.Page, order *model.Order) ([]*model.MediaItem, model.PageResponse, error) {
|
||||
// Initial User Filter
|
||||
tx := dbm.db.Session(&gorm.Session{}).Model(&model.MediaItem{}).Where("user_id == ?", userID)
|
||||
|
||||
// Dynamically Generate Base Query
|
||||
tx, pageResponse := dbm.generateBaseQuery(tx, filters, page, order)
|
||||
|
||||
// Acquire Results
|
||||
var mediaItems []*model.MediaItem
|
||||
err := tx.Find(&mediaItems).Error
|
||||
return mediaItems, pageResponse, err
|
||||
}
|
||||
|
@ -1 +1,37 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (dbm *DBManager) CreateTag(tag *model.Tag) error {
|
||||
log.Debug("[db] Creating tag: ", tag.Name)
|
||||
err := dbm.db.Create(tag).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) Tag(tag *model.Tag) (int64, error) {
|
||||
var count int64
|
||||
err := dbm.db.Where(tag).First(tag).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) Tags(userID string, filters *model.TagFilter, page *model.Page, order *model.Order) ([]*model.Tag, model.PageResponse, error) {
|
||||
// Initial User Filter
|
||||
tx := dbm.db.Session(&gorm.Session{}).Model(&model.Tag{}).Where("user_id == ?", userID)
|
||||
|
||||
// Dynamically Generate Base Query
|
||||
tx, pageResponse := dbm.generateBaseQuery(tx, filters, page, order)
|
||||
|
||||
// Acquire Results
|
||||
var foundTags []*model.Tag
|
||||
err := tx.Find(&foundTags).Error
|
||||
return foundTags, pageResponse, err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) DeleteTag(tag *model.Tag) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -1,35 +1,48 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"reichard.io/imagini/internal/models"
|
||||
"reichard.io/imagini/graph/model"
|
||||
)
|
||||
|
||||
func (dbm *DBManager) CreateUser(user *models.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
|
||||
}
|
||||
user.Password = string(hashedPassword)
|
||||
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 *models.User) (models.User, error) {
|
||||
var foundUser models.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) DeleteUser (user models.User) error {
|
||||
return nil
|
||||
func (dbm *DBManager) Users(filters *model.UserFilter, page *model.Page, order *model.Order) ([]*model.User, model.PageResponse, error) {
|
||||
// Initial User Filter
|
||||
tx := dbm.db.Session(&gorm.Session{}).Model(&model.Tag{})
|
||||
|
||||
// Dynamically Generate Base Query
|
||||
tx, pageResponse := dbm.generateBaseQuery(tx, filters, page, order)
|
||||
|
||||
// Acquire Results
|
||||
var foundUsers []*model.User
|
||||
err := tx.Find(&foundUsers).Error
|
||||
return foundUsers, pageResponse, err
|
||||
}
|
||||
|
||||
func (dbm *DBManager) UpdatePassword (user models.User, pw string) {
|
||||
func (dbm *DBManager) DeleteUser(user model.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbm *DBManager) UpdatePassword(user model.User, pw string) {
|
||||
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
package models
|
||||
|
||||
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,96 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"strings"
|
||||
"reflect"
|
||||
"gorm.io/gorm"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;primarykey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
func (base *Base) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
base.UUID = uuid.New()
|
||||
return
|
||||
}
|
||||
|
||||
type ServerSetting struct {
|
||||
Base
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Description string `json:"description" gorm:"not null"`
|
||||
Value string `json:"value" gorm:"not null"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
Base
|
||||
UserUUID uuid.UUID `json:"-" gorm:"not null"`
|
||||
User User `json:"user" gorm:"ForeignKey:UUID;References:UserUUID;not null"` // User
|
||||
Name string `json:"name" gorm:"not null"` // Name of Device
|
||||
Type string `json:"type" gorm:"not null"` // Android, iOS, Chrome, FireFox, Edge
|
||||
RefreshKey string `json:"-"` // Device Specific Refresh Key
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Base
|
||||
Email string `json:"email" gorm:"unique"` // Email
|
||||
Username string `json:"username" gorm:"unique"` // Username
|
||||
FirstName string `json:"first_name"` // First Name
|
||||
LastName string `json:"last_name"` // Last Name
|
||||
Role string `json:"role"` // Role
|
||||
AuthType string `json:"auth_type" gorm:"default:Local;not null"` // Auth Type (E.g. Local, LDAP)
|
||||
Password string `json:"-"` // Hased & Salted Password
|
||||
}
|
||||
|
||||
type MediaItem struct {
|
||||
Base
|
||||
UserUUID uuid.UUID `json:"-" gorm:"not null"`
|
||||
User User `json:"-" gorm:"ForeignKey:UUID;References:UserUUID;not null"` // User
|
||||
EXIFDate time.Time `json:"exif_date"` // EXIF Date
|
||||
Latitude *float32 `json:"latitude" gorm:"type:decimal(10,2)"` // Decimal Latitude
|
||||
Longitude *float32 `json:"longitude" gorm:"type:decimal(10,2)"` // Decimal Longitude
|
||||
MediaType string `json:"media_type" gorm:"default:Image;not null"` // Image, Video
|
||||
OrigName string `json:"orig_name" gorm:"not null"` // Original Name
|
||||
FileName string `json:"file_name" gorm:"not null"` // File Name
|
||||
Tags []Tag `json:"tags" gorm:"many2many:media_tags;"` // Associated Tag UUIDs
|
||||
Albums []Album `json:"albums" gorm:"many2many:media_albums;"` // Associated Album UUIDs
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Base
|
||||
Name string `json:"name" gorm:"not null"` // Tag Name
|
||||
}
|
||||
|
||||
type Album struct {
|
||||
Base
|
||||
Name string `json:"name" gorm:"not null"` // Album Name
|
||||
}
|
||||
|
||||
func JSONFields(model interface{}) map[string]struct{} {
|
||||
jsonFields := make(map[string]struct{})
|
||||
val := reflect.ValueOf(model)
|
||||
t := val.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
jsonField := strings.TrimSpace(t.Field(i).Tag.Get("json"))
|
||||
|
||||
if jsonField == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
jsonSplit := strings.Split(jsonField, ",")
|
||||
fieldVal := strings.TrimSpace(jsonSplit[0])
|
||||
|
||||
if fieldVal == "" || fieldVal == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
jsonFields[fieldVal] = struct{}{}
|
||||
}
|
||||
|
||||
return jsonFields
|
||||
}
|
@ -1,38 +1,40 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Used to maintain a cache of user specific jwt secrets
|
||||
// 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 {
|
||||
mutex sync.Mutex
|
||||
values map[string]string
|
||||
mutex sync.Mutex
|
||||
values map[string]string
|
||||
}
|
||||
|
||||
func NewMgr() *SessionManager {
|
||||
return &SessionManager{}
|
||||
return &SessionManager{}
|
||||
}
|
||||
|
||||
func (sm *SessionManager) Set(key, value string) {
|
||||
sm.mutex.Lock()
|
||||
sm.values[key] = value
|
||||
sm.mutex.Unlock()
|
||||
sm.mutex.Lock()
|
||||
sm.values[key] = value
|
||||
sm.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (sm *SessionManager) Get(key string) string {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
return sm.values[key]
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
return sm.values[key]
|
||||
}
|
||||
|
||||
func (sm *SessionManager) Delete(key string) {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
_, exists := sm.values[key]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
delete(sm.values, key)
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
_, exists := sm.values[key]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
delete(sm.values, key)
|
||||
}
|
||||
|
240
plugin/models.go
Normal file
240
plugin/models.go
Normal file
@ -0,0 +1,240 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/types"
|
||||
"sort"
|
||||
|
||||
"github.com/99designs/gqlgen/codegen/config"
|
||||
"github.com/99designs/gqlgen/codegen/templates"
|
||||
"github.com/99designs/gqlgen/plugin"
|
||||
"github.com/vektah/gqlparser/v2/ast"
|
||||
)
|
||||
|
||||
type BuildMutateHook = func(b *ModelBuild) *ModelBuild
|
||||
|
||||
func defaultBuildMutateHook(b *ModelBuild) *ModelBuild {
|
||||
return b
|
||||
}
|
||||
|
||||
type ModelBuild struct {
|
||||
PackageName string
|
||||
Interfaces []*Interface
|
||||
Models []*Object
|
||||
Enums []*Enum
|
||||
Scalars []string
|
||||
}
|
||||
|
||||
type Interface struct {
|
||||
Description string
|
||||
Name string
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
Description string
|
||||
Name string
|
||||
Fields []*Field
|
||||
Implements []string
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
Description string
|
||||
Name string
|
||||
Type types.Type
|
||||
Tag string
|
||||
Gorm string
|
||||
}
|
||||
|
||||
type Enum struct {
|
||||
Description string
|
||||
Name string
|
||||
Values []*EnumValue
|
||||
}
|
||||
|
||||
type EnumValue struct {
|
||||
Description string
|
||||
Name string
|
||||
}
|
||||
|
||||
func New() plugin.Plugin {
|
||||
return &Plugin{
|
||||
MutateHook: defaultBuildMutateHook,
|
||||
}
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
MutateHook BuildMutateHook
|
||||
}
|
||||
|
||||
var _ plugin.ConfigMutator = &Plugin{}
|
||||
|
||||
func (m *Plugin) Name() string {
|
||||
return "imaginimodel"
|
||||
}
|
||||
|
||||
func (m *Plugin) MutateConfig(cfg *config.Config) error {
|
||||
binder := cfg.NewBinder()
|
||||
|
||||
b := &ModelBuild{
|
||||
PackageName: cfg.Model.Package,
|
||||
}
|
||||
|
||||
for _, schemaType := range cfg.Schema.Types {
|
||||
if schemaType.BuiltIn {
|
||||
continue
|
||||
}
|
||||
switch schemaType.Kind {
|
||||
case ast.Interface, ast.Union:
|
||||
it := &Interface{
|
||||
Description: schemaType.Description,
|
||||
Name: schemaType.Name,
|
||||
}
|
||||
|
||||
b.Interfaces = append(b.Interfaces, it)
|
||||
case ast.Object, ast.InputObject:
|
||||
if schemaType == cfg.Schema.Query || schemaType == cfg.Schema.Mutation || schemaType == cfg.Schema.Subscription {
|
||||
continue
|
||||
}
|
||||
it := &Object{
|
||||
Description: schemaType.Description,
|
||||
Name: schemaType.Name,
|
||||
}
|
||||
for _, implementor := range cfg.Schema.GetImplements(schemaType) {
|
||||
it.Implements = append(it.Implements, implementor.Name)
|
||||
}
|
||||
|
||||
for _, field := range schemaType.Fields {
|
||||
var typ types.Type
|
||||
fieldDef := cfg.Schema.Types[field.Type.Name()]
|
||||
|
||||
if cfg.Models.UserDefined(field.Type.Name()) {
|
||||
var err error
|
||||
typ, err = binder.FindTypeFromName(cfg.Models[field.Type.Name()].Model[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
switch fieldDef.Kind {
|
||||
case ast.Scalar:
|
||||
// no user defined model, referencing a default scalar
|
||||
typ = types.NewNamed(
|
||||
types.NewTypeName(0, cfg.Model.Pkg(), "string", nil),
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
case ast.Interface, ast.Union:
|
||||
// no user defined model, referencing a generated interface type
|
||||
typ = types.NewNamed(
|
||||
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
|
||||
types.NewInterfaceType([]*types.Func{}, []types.Type{}),
|
||||
nil,
|
||||
)
|
||||
|
||||
case ast.Enum:
|
||||
// no user defined model, must reference a generated enum
|
||||
typ = types.NewNamed(
|
||||
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
case ast.Object, ast.InputObject:
|
||||
// no user defined model, must reference a generated struct
|
||||
typ = types.NewNamed(
|
||||
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
|
||||
types.NewStruct(nil, nil),
|
||||
nil,
|
||||
)
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("unknown ast type %s", fieldDef.Kind))
|
||||
}
|
||||
}
|
||||
|
||||
name := field.Name
|
||||
if nameOveride := cfg.Models[schemaType.Name].Fields[field.Name].FieldName; nameOveride != "" {
|
||||
name = nameOveride
|
||||
}
|
||||
|
||||
typ = binder.CopyModifiersFromAst(field.Type, typ)
|
||||
|
||||
if isStruct(typ) && (fieldDef.Kind == ast.Object || fieldDef.Kind == ast.InputObject) {
|
||||
typ = types.NewPointer(typ)
|
||||
}
|
||||
|
||||
gormType := ""
|
||||
directive := field.Directives.ForName("meta")
|
||||
if directive != nil {
|
||||
arg := directive.Arguments.ForName("gorm")
|
||||
if arg != nil {
|
||||
gormType = fmt.Sprintf("gorm:\"%s\"", arg.Value.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
it.Fields = append(it.Fields, &Field{
|
||||
Name: name,
|
||||
Type: typ,
|
||||
Description: field.Description,
|
||||
Tag: `json:"` + field.Name + `"`,
|
||||
Gorm: gormType,
|
||||
})
|
||||
}
|
||||
|
||||
b.Models = append(b.Models, it)
|
||||
case ast.Enum:
|
||||
it := &Enum{
|
||||
Name: schemaType.Name,
|
||||
Description: schemaType.Description,
|
||||
}
|
||||
|
||||
for _, v := range schemaType.EnumValues {
|
||||
it.Values = append(it.Values, &EnumValue{
|
||||
Name: v.Name,
|
||||
Description: v.Description,
|
||||
})
|
||||
}
|
||||
|
||||
b.Enums = append(b.Enums, it)
|
||||
case ast.Scalar:
|
||||
b.Scalars = append(b.Scalars, schemaType.Name)
|
||||
}
|
||||
}
|
||||
sort.Slice(b.Enums, func(i, j int) bool { return b.Enums[i].Name < b.Enums[j].Name })
|
||||
sort.Slice(b.Models, func(i, j int) bool { return b.Models[i].Name < b.Models[j].Name })
|
||||
sort.Slice(b.Interfaces, func(i, j int) bool { return b.Interfaces[i].Name < b.Interfaces[j].Name })
|
||||
|
||||
for _, it := range b.Enums {
|
||||
cfg.Models.Add(it.Name, cfg.Model.ImportPath()+"."+templates.ToGo(it.Name))
|
||||
}
|
||||
for _, it := range b.Models {
|
||||
cfg.Models.Add(it.Name, cfg.Model.ImportPath()+"."+templates.ToGo(it.Name))
|
||||
}
|
||||
for _, it := range b.Interfaces {
|
||||
cfg.Models.Add(it.Name, cfg.Model.ImportPath()+"."+templates.ToGo(it.Name))
|
||||
}
|
||||
for _, it := range b.Scalars {
|
||||
cfg.Models.Add(it, "github.com/99designs/gqlgen/graphql.String")
|
||||
}
|
||||
|
||||
if len(b.Models) == 0 && len(b.Enums) == 0 && len(b.Interfaces) == 0 && len(b.Scalars) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.MutateHook != nil {
|
||||
b = m.MutateHook(b)
|
||||
}
|
||||
|
||||
return templates.Render(templates.Options{
|
||||
PackageName: cfg.Model.Package,
|
||||
Filename: cfg.Model.Filename,
|
||||
Data: b,
|
||||
GeneratedHeader: true,
|
||||
Packages: cfg.Packages,
|
||||
})
|
||||
}
|
||||
|
||||
func isStruct(t types.Type) bool {
|
||||
_, is := t.Underlying().(*types.Struct)
|
||||
return is
|
||||
}
|
85
plugin/models.gotpl
Normal file
85
plugin/models.gotpl
Normal file
@ -0,0 +1,85 @@
|
||||
{{ reserveImport "context" }}
|
||||
{{ reserveImport "fmt" }}
|
||||
{{ reserveImport "io" }}
|
||||
{{ reserveImport "strconv" }}
|
||||
{{ reserveImport "time" }}
|
||||
{{ reserveImport "sync" }}
|
||||
{{ reserveImport "errors" }}
|
||||
{{ reserveImport "bytes" }}
|
||||
|
||||
{{ reserveImport "github.com/vektah/gqlparser/v2" }}
|
||||
{{ reserveImport "github.com/vektah/gqlparser/v2/ast" }}
|
||||
{{ reserveImport "github.com/99designs/gqlgen/graphql" }}
|
||||
{{ reserveImport "github.com/99designs/gqlgen/graphql/introspection" }}
|
||||
|
||||
{{- range $model := .Interfaces }}
|
||||
{{ with .Description }} {{.|prefixLines "// "}} {{ end }}
|
||||
type {{.Name|go }} interface {
|
||||
Is{{.Name|go }}()
|
||||
}
|
||||
{{- end }}
|
||||
|
||||
{{ range $model := .Models }}
|
||||
{{with .Description }} {{.|prefixLines "// "}} {{end}}
|
||||
type {{ .Name|go }} struct {
|
||||
{{- range $field := .Fields }}
|
||||
{{- with .Description }}
|
||||
{{.|prefixLines "// "}}
|
||||
{{- end}}
|
||||
{{ $field.Name|go }} {{$field.Type | ref}} `{{$field.Tag}} {{$field.Gorm}}`
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
{{- range $iface := .Implements }}
|
||||
func ({{ $model.Name|go }}) Is{{ $iface|go }}() {}
|
||||
{{- end }}
|
||||
{{- end}}
|
||||
|
||||
{{ range $enum := .Enums }}
|
||||
{{ with .Description }} {{.|prefixLines "// "}} {{end}}
|
||||
type {{.Name|go }} string
|
||||
const (
|
||||
{{- range $value := .Values}}
|
||||
{{- with .Description}}
|
||||
{{.|prefixLines "// "}}
|
||||
{{- end}}
|
||||
{{ $enum.Name|go }}{{ .Name|go }} {{$enum.Name|go }} = {{.Name|quote}}
|
||||
{{- end }}
|
||||
)
|
||||
|
||||
var All{{.Name|go }} = []{{ .Name|go }}{
|
||||
{{- range $value := .Values}}
|
||||
{{$enum.Name|go }}{{ .Name|go }},
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
func (e {{.Name|go }}) IsValid() bool {
|
||||
switch e {
|
||||
case {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ $enum.Name|go }}{{ $element.Name|go }}{{end}}:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e {{.Name|go }}) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *{{.Name|go }}) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = {{ .Name|go }}(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid {{ .Name }}", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e {{.Name|go }}) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
{{- end }}
|
Reference in New Issue
Block a user