diff --git a/cmd/imagini.db b/cmd/imagini.db deleted file mode 100644 index eca2661..0000000 Binary files a/cmd/imagini.db and /dev/null differ diff --git a/cmd/server/server.go b/cmd/server/server.go index ba5ed4d..cb22059 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -20,15 +20,15 @@ type Server struct { } func NewServer() *Server { - config := config.Load() - db := db.NewMgr(config) - auth := auth.NewMgr(db) + c := config.Load() + db := db.NewMgr(c) + auth := auth.NewMgr(db, c) api := api.NewApi(db, auth) return &Server{ API: api, Auth: auth, - Config: config, + Config: c, Database: db, } } diff --git a/go.mod b/go.mod index ea00570..f09b8bf 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.15 require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/disintegration/imaging v1.6.2 // indirect + github.com/google/uuid v1.1.5 + 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 diff --git a/go.sum b/go.sum index 2228345..6c00365 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I= +github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 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= @@ -72,6 +74,17 @@ 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/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= +github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911 h1:FvnrqecqX4zT0wOIbYK1gNgTm0677INEWiFY8UEYggY= +github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.0.8 h1:Mj/2Ey9rkGx4w5IMQ2Q+9KLZn4cZoMgKrnMxi9eXE3k= +github.com/lestrrat-go/jwx v1.0.8/go.mod h1:6XJ5sxHF5U116AxYxeHfTnfsZRMgmeKY214zwZDdvho= +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/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= @@ -83,6 +96,8 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uY 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= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -112,6 +127,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ 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= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tus/tusd v1.4.0 h1:8+TmY/Ip2mxO3UlYzrqUkQWMd/FmSSdcIrlJHrr+3HQ= github.com/tus/tusd v1.4.0/go.mod h1:ygrT4B9ZSb27dx3uTnobX5nOFDnutBL6iWKLH4+KpA0= github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e h1:GSGeB9EAKY2spCABz6xOX5DbxZEXolK+nBSvmsQwRjM= @@ -121,6 +138,7 @@ github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX 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/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= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -130,6 +148,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90Pveol golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -142,6 +162,7 @@ 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.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= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -150,6 +171,8 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -158,6 +181,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -182,6 +206,11 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3 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-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-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/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= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -210,6 +239,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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= diff --git a/internal/api/api.go b/internal/api/api.go index b6da92d..e161e21 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -15,9 +15,9 @@ type API struct { func NewApi(db *db.DBManager, auth *auth.AuthManager) *API { api := &API{ - Router: http.NewServeMux(), - DB: db, - Auth: auth, + Router: http.NewServeMux(), + Auth: auth, + DB: db, } api.registerRoutes() return api diff --git a/internal/api/auth.go b/internal/api/auth.go index bde410d..b9c276e 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -35,22 +35,25 @@ func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) { return } - // TODO: Is user already logged in? If so refresh token, if different user, kill session and log in new user? - // Do login resp := api.Auth.AuthenticateUser(creds) - if resp == true { - // Return Success - cookie := http.Cookie{ - Name: "Token", - Value: "testToken", - } - http.SetCookie(w, &cookie) - successJSON(w, "Login success.", http.StatusOK) - }else { - // Return Failure + if !resp { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) + return } + + // Create tokens + accessToken := api.Auth.CreateJWTAccessToken() + refreshToken := api.Auth.CreateRefreshToken() + + // Set appropriate cookies + accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken} + refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken} + http.SetCookie(w, &accessCookie) + http.SetCookie(w, &refreshCookie) + + // Response success + successJSON(w, "Login success.", http.StatusOK) } func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) { @@ -73,3 +76,20 @@ func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) { } http.SetCookie(w, cookie) } + +func (api *API) refreshLoginHandler(w http.ResponseWriter, r *http.Request) { + ok := api.Auth.ValidateRefreshToken() + if !ok { + // TODO: Clear Access & Refresh Cookies + errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) + return + } + + // Update token + accessToken := api.Auth.CreateJWTAccessToken() + accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken} + http.SetCookie(w, &accessCookie) + + // Response success + successJSON(w, "Refresh success.", http.StatusOK) +} diff --git a/internal/api/middlewares.go b/internal/api/middlewares.go index faeca8d..5ffbcb2 100644 --- a/internal/api/middlewares.go +++ b/internal/api/middlewares.go @@ -2,13 +2,14 @@ package api import ( "net/http" - "log" "os" + + log "github.com/sirupsen/logrus" ) -type Middleware func(http.Handler) http.Handler +type Middleware func(http.Handler) http.HandlerFunc -func MultipleMiddleware(h http.Handler, m ...Middleware) http.Handler { +func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc { if len(m) < 1 { return h } @@ -19,19 +20,33 @@ func MultipleMiddleware(h http.Handler, m ...Middleware) http.Handler { return wrapped } -// func authMiddleware(h http.Handler) http.Handler { -// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// _, ok := ValidateUserToken(r) -// -// if ok { -// next.ServeHTTP(w, r) -// } else { -// w.WriteHeader(http.StatusUnauthorized) -// } -// }) -// } +func (api *API) authMiddleware(next http.Handler) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("Token") + if err != nil { + log.Warn("[middleware] Cookie not found") + w.WriteHeader(http.StatusUnauthorized) + return + } -func logMiddleware(h http.Handler) http.Handler { + // Validate cookie.Value JWT with + api.Auth.ValidateJWTToken(cookie.Value) + + + log.Info("[middleware] Cookie Name: ", cookie.Name) + log.Info("[middleware] Cookie Value: ", cookie.Value) + + next.ServeHTTP(w, r) + + // if true { + // next.ServeHTTP(w, r) + // } else { + // w.WriteHeader(http.StatusUnauthorized) + // } + }) +} + +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) diff --git a/internal/api/routes.go b/internal/api/routes.go index 7789dc9..53817bf 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -6,15 +6,38 @@ import ( ) func (api *API) registerRoutes() { - api.Router.HandleFunc("/MediaItems", api.mediaItemsHandler) - api.Router.HandleFunc("/Upload", api.uploadHandler) - api.Router.HandleFunc("/Albums", api.albumsHandler) - api.Router.HandleFunc("/Logout", api.logoutHandler) - api.Router.HandleFunc("/Login", api.loginHandler) - api.Router.HandleFunc("/Users", api.usersHandler) - api.Router.HandleFunc("/Tags", api.tagsHandler) - api.Router.HandleFunc("/Info", api.infoHandler) - api.Router.HandleFunc("/Me", api.meHandler) + api.Router.HandleFunc("/MediaItems", multipleMiddleware( + api.mediaItemsHandler, + api.authMiddleware, + )) + api.Router.HandleFunc("/Upload", multipleMiddleware( + api.uploadHandler, + api.authMiddleware, + )) + api.Router.HandleFunc("/Albums", multipleMiddleware( + api.albumsHandler, + api.authMiddleware, + )) + api.Router.HandleFunc("/Users", multipleMiddleware( + api.usersHandler, + api.authMiddleware, + )) + api.Router.HandleFunc("/Tags", multipleMiddleware( + api.tagsHandler, + api.authMiddleware, + )) + api.Router.HandleFunc("/Info", multipleMiddleware( + api.infoHandler, + api.authMiddleware, + )) + api.Router.HandleFunc("/Me", multipleMiddleware( + api.meHandler, + api.authMiddleware, + )) + + api.Router.HandleFunc("/Logout", api.logoutHandler) + api.Router.HandleFunc("/Login", api.loginHandler) + api.Router.HandleFunc("/RefreshLogin", api.refreshLoginHandler) } // https://stackoverflow.com/a/59764037 diff --git a/internal/api/users.go b/internal/api/users.go index 31faa23..0220829 100644 --- a/internal/api/users.go +++ b/internal/api/users.go @@ -31,9 +31,9 @@ func (api *API) meHandler(w http.ResponseWriter, r *http.Request) { // Get Authenticated User & Return Object authCookie, err := r.Cookie("Token") if err != nil { - log.Error("[routes] ", err) + log.Error("[api] ", err) return } - log.Info("[routes] INFO: ", authCookie) + log.Info("[api] Auth Cookie: ", authCookie) } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 497526d..a6fce77 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -1,20 +1,37 @@ package auth import ( - "errors" + "errors" + + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" + log "github.com/sirupsen/logrus" "gorm.io/gorm" - "reichard.io/imagini/internal/db" - "reichard.io/imagini/internal/models" - log "github.com/sirupsen/logrus" + "reichard.io/imagini/internal/db" + "reichard.io/imagini/internal/config" + "reichard.io/imagini/internal/models" + "reichard.io/imagini/internal/session" + + "encoding/json" + "fmt" + "time" + + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwt" ) type AuthManager struct { - DB *db.DBManager + DB *db.DBManager + Config *config.Config + Session *session.SessionManager } -func NewMgr(db *db.DBManager) *AuthManager { +func NewMgr(db *db.DBManager, c *config.Config) *AuthManager { + session := session.NewMgr() return &AuthManager{ DB: db, + Config: c, + Session: session, } } @@ -46,3 +63,123 @@ func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) bool { return false } } + +func (auth *AuthManager) ValidateJWTToken(userJWT string) bool { + byteUserJWT := []byte(userJWT) + + serverToken, err := jwt.ParseBytes(byteUserJWT, jwt.WithVerify(jwa.HS256, auth.Config.JWTSecret)) + if err != nil { + fmt.Println("failed to parse payload: ", err) + } + + uid, ok := serverToken.Get("uid"); + if !ok { + fmt.Println("failed to acquire uid") + } + + userID := fmt.Sprintf("%v", uid) + userKey := auth.Session.Get(userID) + + userToken, err := jwt.ParseBytes(byteUserJWT, jwt.WithVerify(jwa.HS256, userKey)) + if err != nil { + fmt.Println("failed to parse payload: ", err) + } + _ = userToken + + // TODO: + // - Get User ID from UNVALIDATED token + // - Lookup user key, concat with server key + // - Validate with concatted user & server key + // validatedToken, err := jwt.ParseBytes(byteUserJWT, jwt.WithVerify(jwa.HS256, concatKey)) + // if err != nil { + // fmt.Printf("failed to parse payload: %s\n", err) + // } + + // userToken := auth.Session.Get(userID) + // log.Info("[auth] DEBUG: ", userToken) + + return false +} + +func (auth *AuthManager) RevokeRefreshToken() { + +} + +func (auth *AuthManager) ValidateRefreshToken(refreshToken, deviceID string) bool { + // Acquire Device + deviceUUID, err := uuid.Parse(deviceID) + device := models.Device{Base: models.Base{UUID: deviceUUID}} + foundDevice, err := auth.DB.Device(device) + + // Validate Expiration + expTime, err := time.Parse(time.RFC3339, foundDevice.RefreshExp) + if expTime.Before(time.Now()) { + return false + } + + // Validate Token + bRefreshToken :=[]byte(refreshToken) + err = bcrypt.CompareHashAndPassword([]byte(foundDevice.RefreshToken), bRefreshToken) + if err == nil { + log.Info("[auth] Refresh Token validation succeeded: ", foundDevice.UUID) + return true + } + log.Warn("[auth] Refresh Token validation failed: ", foundDevice.UUID) + return false +} + +func (auth *AuthManager) UpdateRefreshToken(deviceID string) error { + // TODO: + // - Remove Refresh token from Session AND DB + // - Call CreateRefreshToken + return nil +} + +func (auth *AuthManager) CreateRefreshToken(deviceID string) (string, error) { + // TODO: + // - Create regular bcrypt password + // - Create Expiration (Depends on Device Type) + // - Store in DB: DeviceID, ValidUntil + + generatedToken := uuid.New().String() + hashedRefreshToken, err := bcrypt.GenerateFromPassword([]byte(generatedToken), bcrypt.DefaultCost) + if err != nil { + log.Error(err) + return "", err + } + _ = string(hashedRefreshToken) + + return "", nil +} + +func (auth *AuthManager) CreateJWTAccessToken(user, role, deviceID string) (string, error) { + // Create New Token + tm := time.Now() + t := jwt.New() + t.Set(`did`, deviceID) // Device ID + t.Set(`role`, role) // User Role (Admin / User) + t.Set(jwt.SubjectKey, user) // User ID + t.Set(jwt.AudienceKey, `imagini`) // App ID + t.Set(jwt.IssuedAtKey, tm) // Issued At + t.Set(jwt.ExpirationKey, tm.Add(time.Minute * 30)) // 30 Minute 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 +} diff --git a/internal/db/db.go b/internal/db/db.go index cea04dc..8963b9a 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -34,6 +34,7 @@ func NewMgr(c *config.Config) *DBManager { // 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{}) @@ -83,71 +84,3 @@ func (dbm *DBManager) ItemsFromAlbum(user models.User, album models.Album) []mod // INNER JOIN MediaItems ON MediaAlbums.mediaID = MediaItems.mediaID // WHERE MediaAlbums.albumID = ? AND MediaItems.userID = ?`, albumID, userID) } - -// func ItemsFromTags(userID int, tagID int) []MediaItem { -// return nil -// } -// -// func IndexMediaItems(newItems []MediaItem) { -// } - -// func PopulateTestData() { -// user1 := User{Username: "Evan", Email: "evan@reichard.io", FirstName: "Evan", LastName: "Reichard", AuthType: "LDAP", Salt: "1234", HashedPWSalt: "1234"} -// user2 := User{Username: "Ryan", Email: "ryan@example.com", FirstName: "Ryan", LastName: "Dunfrey", AuthType: "Local", Salt: "2345", HashedPWSalt: "2345"} -// user3 := User{Username: "Bill", Email: "bill@example.com", FirstName: "Bill", LastName: "Smith", AuthType: "LDAP", Salt: "3456", HashedPWSalt: "3456"} -// -// mi1 := MediaItem{ -// User: user1, -// EXIFDate: time.Now(), -// Latitude: "1234", -// Longitude: "1234", -// RelPath: "./1234.jpg", -// Tags: []Tag{ -// {Name: "Tag1"}, -// {Name: "Tag2"}, -// }, -// Albums: []Album{ -// {Name: "Album1"}, -// {Name: "Album2"}, -// }, -// } -// -// mi2 := MediaItem{ -// User: user2, -// EXIFDate: time.Now(), -// Latitude: "1234", -// Longitude: "1234", -// RelPath: "./1234.jpg", -// Tags: []Tag{ -// {Name: "Tag3"}, -// {Name: "Tag4"}, -// }, -// Albums: []Album{ -// {Name: "Album3"}, -// {Name: "Album4"}, -// }, -// } -// -// mi3 := MediaItem{ -// User: user3, -// EXIFDate: time.Now(), -// Latitude: "1234", -// Longitude: "1234", -// RelPath: "./1234.jpg", -// Tags: []Tag{ -// {Name: "Tag4"}, -// {Name: "Tag5"}, -// }, -// Albums: []Album{ -// {Name: "Album1"}, -// {Name: "Album7"}, -// }, -// } -// -// // db.Create(&user1) -// // db.Create(&user2) -// // db.Create(&user3) -// db.Create(&mi1) -// db.Create(&mi2) -// db.Create(&mi3) -// } diff --git a/internal/db/devices.go b/internal/db/devices.go new file mode 100644 index 0000000..3d8728d --- /dev/null +++ b/internal/db/devices.go @@ -0,0 +1,44 @@ +package db + +import ( + "errors" + "gorm.io/gorm" + "golang.org/x/crypto/bcrypt" + log "github.com/sirupsen/logrus" + + "reichard.io/imagini/internal/models" +) + +func (dbm *DBManager) CreateDevice(device models.Device) error { + log.Info("[query] Creating device: ", device.Name) + _, err := dbm.Device(device) + if !errors.Is(err, gorm.ErrRecordNotFound) { + log.Warn("[query] Device already exists: ", device.Name) + return errors.New("Device already exists") + } + + // Generate random password + refreshToken := "asd123" + hashedToken, err := bcrypt.GenerateFromPassword([]byte(refreshToken), bcrypt.DefaultCost) + if err != nil { + log.Error(err) + return err + } + device.RefreshToken = string(hashedToken) + return dbm.db.Create(&device).Error +} + +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) DeleteDevice (user models.Device) error { + return nil +} + +func (dbm *DBManager) UpdateRefreshToken (device models.Device, refreshToken string) error { + return nil +} diff --git a/internal/models/db.go b/internal/models/db.go index 8543698..b6c66e1 100644 --- a/internal/models/db.go +++ b/internal/models/db.go @@ -1,40 +1,53 @@ package models import ( + "github.com/google/uuid" "gorm.io/gorm" "time" ) -// Might not even need this +// Base contains common columns for all tables. +type Base struct { + UUID uuid.UUID `gorm:"type:uuid;default:default:uuid_generate_v4();primarykey"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} + +func (base *Base) BeforeCreate(tx *gorm.DB) (err error) { + base.UUID = uuid.New() + return +} + type ServerSetting struct { - gorm.Model + 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 { - gorm.Model - User User `json:"user" gorm:"ForeignKey:ID"` - DeviceName string `json:"name"` - Type string `json:"type"` // Android, iOS, Chrome, FireFox, Edge, etc + Base + User User `json:"user" gorm:"ForeignKey:UUID"` + Name string `json:"name"` + Type string `json:"type"` // Android, iOS, Chrome, FireFox, Edge, etc + RefreshExp string `json:"refresh_exp"` + RefreshToken string `json:"-"` } -// TODO: ID -> UUID? type User struct { - gorm.Model + Base Email string `json:"email" gorm:"unique"` Username string `json:"username" gorm:"unique"` FirstName string `json:"first_name"` LastName string `json:"last_name"` AuthType string `json:"auth_type" gorm:"default:Local;not null"` Password string `json:"-"` - JWTSecret string `json:"-" gorm:"unique;not null"` // TODO: Auto Generate UUID } type MediaItem struct { - gorm.Model - User User `json:"user" gorm:"ForeignKey:ID;not null"` + Base + User User `json:"user" gorm:"ForeignKey:UUID;not null"` EXIFDate time.Time `json:"exif_date"` Latitude string `json:"latitude"` Longitude string `json:"longitude"` @@ -45,11 +58,11 @@ type MediaItem struct { } type Tag struct { - gorm.Model + Base Name string `json:"name" gorm:"not null"` } type Album struct { - gorm.Model + Base Name string `json:"name" gorm:"not null"` } diff --git a/internal/session/session.go b/internal/session/session.go index 38d0ba6..ea001c9 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -4,8 +4,11 @@ import ( "sync" ) +// Used to maintain a cache of user specific jwt secrets +// This will prevent DB lookups on every request type SessionManager struct { - Mutex sync.Mutex + mutex sync.Mutex + values map[string]string } func NewMgr() *SessionManager { @@ -13,13 +16,23 @@ func NewMgr() *SessionManager { } func (sm *SessionManager) Set(key, value string) { - + sm.mutex.Lock() + sm.values[key] = value + sm.mutex.Unlock() } func (sm *SessionManager) Get(key string) string { - return "" + 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) }