diff --git a/.gitignore b/.gitignore index cedde8a..0e74ab7 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ frontend/build frontend/npm-debug.log* frontend/yarn-debug.log* frontend/yarn-error.log* -backend/embed/assets \ No newline at end of file +backend/embed/assets +.vscode \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod index fbaed8e..414cf6a 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,21 +4,32 @@ go 1.19 require ( github.com/aymerick/raymond v2.0.2+incompatible + github.com/go-chi/jwtauth/v5 v5.0.2 github.com/go-playground/validator/v10 v10.11.1 + github.com/golang-jwt/jwt/v4 v4.4.2 + github.com/lestrrat-go/jwx v1.2.6 + golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 gorm.io/driver/sqlite v1.3.6 gorm.io/gorm v1.23.9 ) require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/goccy/go-json v0.7.6 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/leodido/go-urn v1.2.1 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.0 // indirect + github.com/lestrrat-go/iter v1.0.1 // indirect + github.com/lestrrat-go/option v1.0.0 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/qri-io/jsonpointer v0.1.1 // indirect - golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 82216ca..e78d0e9 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -4,12 +4,18 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/jwtauth/v5 v5.0.2 h1:CSKtr+b6Jnfy5T27sMaiBPxaVE/bjnjS3ramFQ0526w= +github.com/go-chi/jwtauth/v5 v5.0.2/go.mod h1:TeA7vmPe3uYThvHw8O8W13HOOpOd4MTgToxL41gZyjs= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= @@ -18,6 +24,10 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/goccy/go-json v0.7.6 h1:H0wq4jppBQ+9222sk5+hPLL25abZQiRuQ6YPnjO9c+A= +github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 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= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= @@ -31,6 +41,19 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/codegen v1.0.1/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM= +github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc= +github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.6 h1:XAgfuHaOB7fDZ/6WhVgl8K89af768dU+3Nx4DlTbLIk= +github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -41,6 +64,8 @@ github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOj github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA= @@ -53,16 +78,36 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk= github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-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-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -70,16 +115,26 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc= golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/api/context/context.go b/backend/internal/api/context/context.go index d264cf9..de540b1 100644 --- a/backend/internal/api/context/context.go +++ b/backend/internal/api/context/context.go @@ -3,6 +3,8 @@ package context var ( // BodyCtxKey is the name of the Body value on the context BodyCtxKey = &contextKey{"Body"} + // UserIDCtxKey is the name of the UserID value on the context + UserIDCtxKey = &contextKey{"UserID"} // PrettyPrintCtxKey is the name of the pretty print context PrettyPrintCtxKey = &contextKey{"Pretty"} ) diff --git a/backend/internal/api/handler/user.go b/backend/internal/api/handler/user.go new file mode 100644 index 0000000..fe82401 --- /dev/null +++ b/backend/internal/api/handler/user.go @@ -0,0 +1,168 @@ +package handler + +import ( + "encoding/json" + "errors" + "net/http" + + h "github.com/Pacerino/CaddyProxyManager/internal/api/http" + "github.com/Pacerino/CaddyProxyManager/internal/auth" + "github.com/Pacerino/CaddyProxyManager/internal/database" + "github.com/Pacerino/CaddyProxyManager/internal/logger" + "github.com/go-playground/validator/v10" + "gorm.io/gorm" +) + +type LoginData struct { + Email string `validate:"required,email"` + Secret string `validate:"required"` +} + +// UserLogin will login a user and return a JWT Token +// Route: GET /users/login +func (s Handler) UserLogin() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var logindata LoginData + err := json.NewDecoder(r.Body).Decode(&logindata) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + validate := validator.New() + if err := validate.Struct(logindata); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + jwtData, err := auth.PeformLogin(logindata.Email, logindata.Secret) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // If the User wasnt found, throw an error + h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Login incorrect", nil) + return + } else { + // if the user was found but there is an error, throw it also + h.ResultErrorJSON(w, r, http.StatusBadGateway, err.Error(), nil) + return + } + } + + h.ResultResponseJSON(w, r, http.StatusOK, jwtData) + } +} + +// GetUsers will return a list of users +// Route: GET /users +func (s Handler) GetUsers() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var users []database.User + + if err := s.DB.Select([]string{"Name", "Email", "ID", "CreatedAt", "UpdatedAt", "DeletedAt"}).Find(&users).Error; err != nil { + logger.Error("", err) + h.ResultErrorJSON(w, r, http.StatusBadRequest, "could not retrieve user list", nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, users) + } + } +} + +// GetUser will return a list of users +// Route: GET /users/{userID} +func (s Handler) GetUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var userID int + var user database.User + if userID, err = getURLParamInt(r, "userID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if err = s.DB.Where("id = ?", userID).First(&user).Error; err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, user) + } + } +} + +// CreateUser will create a Host +// Route: POST /users +func (s Handler) CreateUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var newUser database.User + err := json.NewDecoder(r.Body).Decode(&newUser) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + validate := validator.New() + if err := validate.Struct(&newUser); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + generatedSecret, err := auth.GenerateSecret(newUser.Secret) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + newUser.Secret = generatedSecret + result := s.DB.Create(&newUser) + if result.Error != nil { + logger.Error("", result.Error) + h.ResultErrorJSON(w, r, http.StatusBadRequest, "could not create user", nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newUser) + } +} + +// DeleteUser removes a host +// Route: DELETE /users/{userID} +func (s Handler) DeleteUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var userID int + if userID, err = getURLParamInt(r, "userID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + result := s.DB.Unscoped().Delete(&database.User{}, userID) + if result.Error != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, result.Error.Error(), nil) + return + } + if result.RowsAffected > 0 { + h.ResultResponseJSON(w, r, http.StatusOK, true) + } else { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrIDNotFound.Error(), nil) + } + } +} + +// UpdateUser updates a user +// Route: PUT /users +func (s Handler) UpdateUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var newUser database.User + err := json.NewDecoder(r.Body).Decode(&newUser) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + validate := validator.New() + if err := validate.Struct(newUser); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + result := s.DB.Updates(&newUser) + if result.Error != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, result.Error.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newUser) + } +} diff --git a/backend/internal/api/middleware/auth.go b/backend/internal/api/middleware/auth.go new file mode 100644 index 0000000..7e5a586 --- /dev/null +++ b/backend/internal/api/middleware/auth.go @@ -0,0 +1,58 @@ +package middleware + +import ( + "context" + "net/http" + + c "github.com/Pacerino/CaddyProxyManager/internal/api/context" + h "github.com/Pacerino/CaddyProxyManager/internal/api/http" + "github.com/Pacerino/CaddyProxyManager/internal/auth" + "github.com/Pacerino/CaddyProxyManager/internal/logger" + "github.com/go-chi/jwtauth/v5" + "github.com/lestrrat-go/jwx/jwt" +) + +// DecodeAuth decodes an auth header +func DecodeAuth() func(http.Handler) http.Handler { + privateKey, privateKeyParseErr := auth.GetPrivateKey() + if privateKeyParseErr != nil && privateKey == nil { + logger.Error("PrivateKeyParseError", privateKeyParseErr) + } + + /* publicKey, publicKeyParseErr := auth.GetPublicKey() + if publicKeyParseErr != nil && publicKey == nil { + logger.Error("PublicKeyParseError", publicKeyParseErr) + } */ + + tokenAuth := jwtauth.New("RS256", privateKey, privateKey.PublicKey) + return jwtauth.Verifier(tokenAuth) +} + +// Enforce is a authentication middleware to enforce access from the +// jwtauth.Verifier middleware request context values. The Authenticator sends a 401 Unauthorised +// response for any unverified tokens and passes the good ones through. +func Enforce() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + token, claims, err := jwtauth.FromContext(ctx) + + if err != nil { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, err.Error(), nil) + return + } + + userID := int(claims["uid"].(float64)) + if token == nil || jwt.Validate(token) != nil { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil) + return + } + + // Add claims to context + ctx = context.WithValue(ctx, c.UserIDCtxKey, userID) + + // Token is authenticated, continue as normal + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go index 50b1e08..2eeceda 100644 --- a/backend/internal/api/router.go +++ b/backend/internal/api/router.go @@ -7,6 +7,7 @@ import ( "github.com/Pacerino/CaddyProxyManager/embed" "github.com/Pacerino/CaddyProxyManager/internal/api/handler" + "github.com/Pacerino/CaddyProxyManager/internal/api/middleware" "github.com/go-chi/chi/v5" "github.com/go-chi/cors" @@ -26,6 +27,7 @@ func NewRouter() http.Handler { r.Use( cors, + middleware.DecodeAuth(), ) return generateRoutes(r, h) @@ -35,13 +37,23 @@ func generateRoutes(r chi.Router, h *handler.Handler) chi.Router { r.Route("/api", func(r chi.Router) { //Hosts - r.Route("/hosts", func(r chi.Router) { + r.With(middleware.Enforce()).Route("/hosts", func(r chi.Router) { r.Get("/", h.GetHosts()) // Get List of Hosts r.Post("/", h.CreateHost()) // Create Host & save to the DB r.Get("/{hostID:[0-9]+}", h.GetHost()) // Get specific Host by ID r.Delete("/{hostID:[0-9]+}", h.DeleteHost()) // Delete Host by ID r.Put("/", h.UpdateHost()) // Update Host by ID }) + + //User + r.Route("/users", func(r chi.Router) { + r.Post("/login", h.UserLogin()) // Login a User + r.With(middleware.Enforce()).Get("/", h.GetUsers()) // Get a list of Users + r.With(middleware.Enforce()).Post("/", h.CreateUser()) // Create a User + r.With(middleware.Enforce()).Get("/{userID:[0-9]+}", h.GetUser()) // Get a User by ID + r.With(middleware.Enforce()).Delete("/{userID:[0-9]+}", h.DeleteUser()) // Delete User by ID + r.With(middleware.Enforce()).Put("/", h.UpdateUser()) // Update Host by ID + }) }) fileServer(r) return r diff --git a/backend/internal/auth/auth.go b/backend/internal/auth/auth.go new file mode 100644 index 0000000..15cc905 --- /dev/null +++ b/backend/internal/auth/auth.go @@ -0,0 +1,42 @@ +package auth + +import ( + "errors" + + "github.com/Pacerino/CaddyProxyManager/internal/database" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +func GenerateSecret(pass string) (string, error) { + if pass == "" { + return "", errors.New("no password has been submitted") + } + hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hash), nil +} + +func PeformLogin(email, pass string) (GeneratedResponse, error) { + var user database.User + var err error + db := database.GetInstance() + // Find user based on email + err = db.Where("email = ?", email).First(&user).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return GeneratedResponse{}, err + } + // Compare stored PW and given PW + err = bcrypt.CompareHashAndPassword([]byte(user.Secret), []byte(pass)) + if err != nil { + return GeneratedResponse{}, errors.New("username or Password is wrong") + } + // Everything seems legit, generate JWT + jwtData, err := Generate(&user) + if err != nil { + return GeneratedResponse{}, err + } + return jwtData, nil +} diff --git a/backend/internal/auth/jwt.go b/backend/internal/auth/jwt.go new file mode 100644 index 0000000..c06480d --- /dev/null +++ b/backend/internal/auth/jwt.go @@ -0,0 +1,60 @@ +package auth + +import ( + "fmt" + "time" + + "github.com/Pacerino/CaddyProxyManager/internal/logger" + + "github.com/Pacerino/CaddyProxyManager/internal/database" + "github.com/golang-jwt/jwt/v4" +) + +// UserJWTClaims is the structure of a JWT for a User +type UserJWTClaims struct { + UserID uint `json:"uid"` + jwt.RegisteredClaims +} + +// GeneratedResponse is the response of a generated token, usually used in http response +type GeneratedResponse struct { + Expires int64 `json:"expires"` + Token string `json:"token"` +} + +// Generate will create a JWT +func Generate(userObj *database.User) (GeneratedResponse, error) { + var response GeneratedResponse + + key, err := GetPrivateKey() + if err != nil { + logger.Error("JWTError", fmt.Errorf("error signing token: %v", err)) + return response, err + } + expires := time.Now().AddDate(0, 0, 1) // 1 day + + // Create the Claims + claims := UserJWTClaims{ + userObj.ID, + jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(expires), + Issuer: "api", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + //var err error + token.Signature, err = token.SignedString(key) + if err != nil { + logger.Error("JWTError", fmt.Errorf("error signing token: %v", err)) + return response, err + } + + response = GeneratedResponse{ + Expires: expires.Unix(), + Token: token.Signature, + } + + return response, nil +} diff --git a/backend/internal/auth/keys.go b/backend/internal/auth/keys.go new file mode 100644 index 0000000..ca83a2c --- /dev/null +++ b/backend/internal/auth/keys.go @@ -0,0 +1,101 @@ +package auth + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "os" + + "github.com/Pacerino/CaddyProxyManager/internal/config" +) + +var ( + privateKey *rsa.PrivateKey + publicKey *rsa.PublicKey +) + +func openKey(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + fileKey, err := io.ReadAll(f) + if err != nil { + return nil, err + } + return fileKey, nil +} + +// GetPrivateKey will load the key from config package and return a usable object +// It should only load from file once per program execution +func GetPrivateKey() (*rsa.PrivateKey, error) { + if privateKey == nil { + var blankKey *rsa.PrivateKey + + var err error + key, err := openKey(fmt.Sprintf("%s/jwtkey.pem", config.Configuration.DataFolder)) + if err != nil { + return blankKey, err + } + privateKey, err = LoadPemPrivateKey(key) + if err != nil { + return blankKey, err + } + } + + /* pub, pubErr := GetPublicKey() + if pubErr != nil { + return privateKey, pubErr + } + + privateKey.PublicKey = *pub */ + + return privateKey, nil +} + +// LoadPemPrivateKey reads a key from a PEM encoded string and returns a private key +func LoadPemPrivateKey(content []byte) (*rsa.PrivateKey, error) { + var key *rsa.PrivateKey + data, _ := pem.Decode(content) + var err error + key, err = x509.ParsePKCS1PrivateKey(data.Bytes) + if err != nil { + return key, err + } + return key, nil +} + +// GetPublicKey will load the key from config package and return a usable object +// It should only load once per program execution +func GetPublicKey() (*rsa.PublicKey, error) { + if publicKey == nil { + var blankKey *rsa.PublicKey + + var err error + key, err := openKey(fmt.Sprintf("%s/jwtkey.pem", config.Configuration.DataFolder)) + if err != nil { + return blankKey, err + } + publicKey, err = LoadPemPublicKey(key) + if err != nil { + return blankKey, err + } + } + + return publicKey, nil +} + +// LoadPemPublicKey reads a key from a PEM encoded string and returns a public key +func LoadPemPublicKey(content []byte) (*rsa.PublicKey, error) { + var key *rsa.PublicKey + data, _ := pem.Decode(content) + publicKeyFileImported, err := x509.ParsePKCS1PublicKey(data.Bytes) + if err != nil { + return key, err + } + + return publicKeyFileImported, nil +} diff --git a/backend/internal/database/models.go b/backend/internal/database/models.go index fd50b18..d43fe12 100644 --- a/backend/internal/database/models.go +++ b/backend/internal/database/models.go @@ -14,3 +14,10 @@ type Upstream struct { HostID uint `json:"hostId"` Backend string `json:"backend" validate:"required,hostname_port"` } + +type User struct { + gorm.Model + Name string `validate:"required"` + Email string `gorm:"unique" validate:"required,email"` + Secret string +} diff --git a/backend/internal/database/sqlite.go b/backend/internal/database/sqlite.go index 09efb6c..b96d9a6 100644 --- a/backend/internal/database/sqlite.go +++ b/backend/internal/database/sqlite.go @@ -16,7 +16,7 @@ var dbInstance *gorm.DB func NewDB() { logger.Info("Creating new DB instance") dbInstance = SqliteDB() - dbInstance.AutoMigrate(&Host{}, &Upstream{}) + dbInstance.AutoMigrate(&Host{}, &Upstream{}, &User{}) } // GetInstance returns an existing or new instance