diff --git a/.github/workflows/auth-service-cd.yml b/.github/workflows/auth-service-cd.yml index 5ad23ae..a7374f7 100644 --- a/.github/workflows/auth-service-cd.yml +++ b/.github/workflows/auth-service-cd.yml @@ -16,10 +16,10 @@ jobs: id: create_image_tag with: script: | - if (context.issue.number) { - return "pr" + context.issue.number; - } else if(github.ref == 'refs/heads/main') { + if(github.ref == 'refs/heads/main') { return 'latest'; + } else if(context.issue.number) { + return "pr" + context.issue.number; } else { return "pr" + ( await github.rest.repos.listPullRequestsAssociatedWithCommit({ diff --git a/.github/workflows/auth-service-ci.yml b/.github/workflows/auth-service-ci.yml index 36749d8..a7bc90e 100644 --- a/.github/workflows/auth-service-ci.yml +++ b/.github/workflows/auth-service-ci.yml @@ -3,10 +3,6 @@ on: push: paths: - "auth-service/**" - pull_request: - paths: - - "auth-service/**" - jobs: lint: uses: ./.github/workflows/go_lint.yml diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index 0245867..16ba9c0 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -16,10 +16,10 @@ jobs: id: create_image_tag with: script: | - if (context.issue.number) { - return "pr" + context.issue.number; - } else if(github.ref == 'refs/heads/main') { + if(github.ref == 'refs/heads/main') { return 'latest'; + } else if(context.issue.number) { + return "pr" + context.issue.number; } else { return "pr" + ( await github.rest.repos.listPullRequestsAssociatedWithCommit({ diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 5ef7000..ebee273 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -3,9 +3,6 @@ on: push: paths: - "backend/**" - pull_request: - paths: - - "backend/**" jobs: lint: uses: ./.github/workflows/go_lint.yml diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index 6ca1c52..209ae60 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -3,10 +3,6 @@ on: push: paths: - "frontend/**" - pull_request: - paths: - - "frontend/**" - jobs: lint: uses: ./.github/workflows/svelte_lint.yml diff --git a/auth-service/go.mod b/auth-service/go.mod index 45ebb40..5e8b64d 100644 --- a/auth-service/go.mod +++ b/auth-service/go.mod @@ -1,3 +1,32 @@ module github.com/vcscsvcscs/GenerationsHeritage/auth-service go 1.22.2 + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.9.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/vcscsvcscs/GenerationsHeritage/utilities v0.0.0-20240414091827-ffde94d457cb // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/auth-service/go.sum b/auth-service/go.sum new file mode 100644 index 0000000..55534de --- /dev/null +++ b/auth-service/go.sum @@ -0,0 +1,80 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vcscsvcscs/GenerationsHeritage/utilities v0.0.0-20240414091827-ffde94d457cb h1:fU736we2gQQRMOWP/su7sCiUFmrXTKBN0s8LG5k7bOE= +github.com/vcscsvcscs/GenerationsHeritage/utilities v0.0.0-20240414091827-ffde94d457cb/go.mod h1:aQlmG6BiGFmOFxzAkWTJDzm1EzdCJ4OEETXTUkWJaLk= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/backend/go.mod b/backend/go.mod index b7df6ff..eb6ce58 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,7 +4,10 @@ go 1.22.2 require ( github.com/gin-gonic/gin v1.9.1 - github.com/vcscsvcscs/GenerationsHeritage/utilities v0.0.0-20240413225529-30321ba5d7e7 + github.com/google/uuid v1.6.0 + github.com/neo4j/neo4j-go-driver/v5 v5.19.0 + github.com/vcscsvcscs/GenerationsHeritage/utilities v0.0.0-20240414091827-ffde94d457cb + golang.org/x/net v0.22.0 ) require ( @@ -28,7 +31,6 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.7.0 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.22.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index aaff72d..10b506e 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -31,6 +31,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -46,6 +48,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/neo4j/neo4j-go-driver/v5 v5.19.0 h1:v2cB19fZQYz1xmj6EZXofFHD/+Tj16hH/OOp39uNN1I= +github.com/neo4j/neo4j-go-driver/v5 v5.19.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -68,6 +72,8 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vcscsvcscs/GenerationsHeritage/utilities v0.0.0-20240413225529-30321ba5d7e7 h1:6HOZdgsOt8KojDfNDOyHLwv+Chv90MECxMdP+cKKNv4= github.com/vcscsvcscs/GenerationsHeritage/utilities v0.0.0-20240413225529-30321ba5d7e7/go.mod h1:8byGXK+Csy5RCmHrvdMIzS8oVuvkr9Ech2PqLrad7os= +github.com/vcscsvcscs/GenerationsHeritage/utilities v0.0.0-20240414091827-ffde94d457cb h1:fU736we2gQQRMOWP/su7sCiUFmrXTKBN0s8LG5k7bOE= +github.com/vcscsvcscs/GenerationsHeritage/utilities v0.0.0-20240414091827-ffde94d457cb/go.mod h1:aQlmG6BiGFmOFxzAkWTJDzm1EzdCJ4OEETXTUkWJaLk= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= diff --git a/backend/handlers/createPerson.go b/backend/handlers/createPerson.go new file mode 100644 index 0000000..a6cb139 --- /dev/null +++ b/backend/handlers/createPerson.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/vcscsvcscs/GenerationsHeritage/backend/memgraph" +) + +func CreatePerson(driver neo4j.DriverWithContext) gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.Body == nil || c.ContentType() != "application/json" { + log.Printf("ip: %s error: request body is empty or content type is not application/json", c.ClientIP()) + c.JSON(http.StatusBadRequest, gin.H{"error": "content type must be application/json and request body must not be empty"}) + + return + } + + var person memgraph.Person + err := json.NewDecoder(c.Request.Body).Decode(&person) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + + return + } + + if err := person.Verify(); err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": "contains-forbidden-characters"}) + + return + } + + rec, err := person.CreatePerson(driver) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": "already-exists"}) + + return + } + + c.JSON(http.StatusCreated, gin.H{"person": rec.AsMap()}) + } +} diff --git a/backend/handlers/createRelationship.go b/backend/handlers/createRelationship.go new file mode 100644 index 0000000..71b28eb --- /dev/null +++ b/backend/handlers/createRelationship.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/vcscsvcscs/GenerationsHeritage/backend/memgraph" +) + +func CreateRelationship(driver neo4j.DriverWithContext) gin.HandlerFunc { + return func(c *gin.Context) { + var relationship memgraph.Relationship + if err := c.ShouldBindJSON(&relationship); err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + + return + } + + rec, err := relationship.CreateRelationship(driver) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + + return + } + + c.JSON(http.StatusCreated, gin.H{"relationship": rec.AsMap()}) + } +} diff --git a/backend/handlers/create_relationship_and_person.go b/backend/handlers/create_relationship_and_person.go new file mode 100644 index 0000000..7747ee3 --- /dev/null +++ b/backend/handlers/create_relationship_and_person.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/vcscsvcscs/GenerationsHeritage/backend/memgraph" +) + +func CreateRelationshipAndPerson(driver neo4j.DriverWithContext) gin.HandlerFunc { + return func(c *gin.Context) { + var rp memgraph.RelationshipAndPerson + if err := c.ShouldBindJSON(&rp); err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + + return + } + + rec, err := rp.CreateRelationshipAndPerson(driver) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + + return + } + + c.JSON(http.StatusCreated, gin.H{"relationship": rec.AsMap()}) + } +} diff --git a/backend/handlers/deletePerson.go b/backend/handlers/deletePerson.go new file mode 100644 index 0000000..48becd0 --- /dev/null +++ b/backend/handlers/deletePerson.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/vcscsvcscs/GenerationsHeritage/backend/memgraph" +) + +func DeletePerson(driver neo4j.DriverWithContext) gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.Body == nil || c.ContentType() != "application/json" { + log.Printf("ip: %s error: request body is empty or content type is not application/json", c.ClientIP()) + c.JSON(http.StatusBadRequest, gin.H{"error": "content type must be application/json and request body must not be empty"}) + + return + } + + var person memgraph.Person + err := json.NewDecoder(c.Request.Body).Decode(&person) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + + return + } + + if person.ID != "" { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusBadRequest, gin.H{"error": "no person ID provided"}) + + return + } + + err = person.DeletePerson(driver) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusNotFound, gin.H{"error": "could not delete person with ID provided"}) + + return + } + + c.JSON(http.StatusOK, gin.H{"status": "person deleted successfully"}) + } +} diff --git a/backend/handlers/deleteRelationship.go b/backend/handlers/deleteRelationship.go new file mode 100644 index 0000000..160019a --- /dev/null +++ b/backend/handlers/deleteRelationship.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/vcscsvcscs/GenerationsHeritage/backend/memgraph" +) + +func DeleteRelationship(driver neo4j.DriverWithContext) gin.HandlerFunc { + return func(c *gin.Context) { + var relationship memgraph.Relationship + if err := c.ShouldBindJSON(&relationship); err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + + return + } + + err := relationship.DeleteRelationship(driver) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + + return + } + + c.JSON(http.StatusAccepted, gin.H{"status": "relationship deleted successfully"}) + } +} diff --git a/backend/handlers/updatePerson.go b/backend/handlers/updatePerson.go new file mode 100644 index 0000000..ce094cb --- /dev/null +++ b/backend/handlers/updatePerson.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/vcscsvcscs/GenerationsHeritage/backend/memgraph" +) + +func UpdatePerson(driver neo4j.DriverWithContext) gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.Body == nil || c.ContentType() != "application/json" { + log.Printf("ip: %s error: request body is empty or content type is not application/json", c.ClientIP()) + c.JSON(http.StatusBadRequest, gin.H{"error": "content type must be application/json and request body must not be empty"}) + + return + } + + var person memgraph.Person + err := json.NewDecoder(c.Request.Body).Decode(&person) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + + return + } + + if person.ID == "" { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusBadRequest, gin.H{"error": "No ID provided"}) + + return + } + + rec, err := person.UpdatePerson(driver) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusNotFound, gin.H{"error": "could not update person with information provided"}) + + return + } + + c.JSON(http.StatusOK, gin.H{"person": rec.AsMap()}) + } +} diff --git a/backend/handlers/verifyRelationship.go b/backend/handlers/verifyRelationship.go new file mode 100644 index 0000000..d347d80 --- /dev/null +++ b/backend/handlers/verifyRelationship.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "github.com/vcscsvcscs/GenerationsHeritage/backend/memgraph" +) + +func VerifyRelationship(driver neo4j.DriverWithContext) gin.HandlerFunc { + return func(c *gin.Context) { + var relationship memgraph.Relationship + if err := c.ShouldBindJSON(&relationship); err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + + return + } + + rec, err := relationship.VerifyRelationship(driver) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + + return + } + + c.JSON(http.StatusOK, gin.H{"relationship": rec.AsMap()}) + } +} diff --git a/backend/handlers/viewFamilyTree.go b/backend/handlers/viewFamilyTree.go new file mode 100644 index 0000000..e082dc4 --- /dev/null +++ b/backend/handlers/viewFamilyTree.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "context" + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +func ViewFamiliyTree(driver neo4j.DriverWithContext) gin.HandlerFunc { + return func(c *gin.Context) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead}) + defer session.Close(ctx) + + id := c.Query("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"}) + + return + } + + query := ` + MATCH (n:Person {ID: $person_id})-[p:Parent*1..]->(family:Person) + OPTIONAL MATCH (family)-[c:Child]->(children:Person) + WITH family, p, children, c, n + OPTIONAL MATCH (children)<-[p2:Parent]-(OtherParents:Person) + WITH family, p, children, c, OtherParents, p2,n + OPTIONAL MATCH (family)-[s:Spouse]-(spouse:Person) + RETURN family, p, children, c, OtherParents, p2, spouse, s, n` + + result, err := session.Run(ctx, query, map[string]any{"person_id": id}) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + + return + } + + rec, err := result.Single(ctx) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusNotFound, gin.H{"error": "could not find family tree for person with id: " + id}) + + return + } + + c.JSON(200, rec.AsMap()["n"]) + } +} diff --git a/backend/handlers/viewPerson.go b/backend/handlers/viewPerson.go new file mode 100644 index 0000000..3e7c3a1 --- /dev/null +++ b/backend/handlers/viewPerson.go @@ -0,0 +1,46 @@ +package handlers + +import ( + "context" + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +func ViewPerson(driver neo4j.DriverWithContext) gin.HandlerFunc { + return func(c *gin.Context) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead}) + defer session.Close(ctx) + + id := c.Query("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"}) + + return + } + + result, err := session.Run(ctx, "MATCH (n:Person) WHERE n.ID = $person_id RETURN n;", map[string]any{"person_id": id}) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + + return + } + + rec, err := result.Single(ctx) + if err != nil { + log.Printf("ip: %s error: %s", c.ClientIP(), err) + c.JSON(http.StatusNotFound, gin.H{"error": "could not find person with information provided"}) + + return + } + + c.JSON(200, rec.AsMap()["n"]) + } +} diff --git a/backend/main.go b/backend/main.go index cb60a60..ce1ca68 100644 --- a/backend/main.go +++ b/backend/main.go @@ -2,18 +2,16 @@ package main import ( "context" - "errors" "flag" - "fmt" - "io" "log" - "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" + "github.com/vcscsvcscs/GenerationsHeritage/backend/handlers" + "github.com/vcscsvcscs/GenerationsHeritage/backend/memgraph" "github.com/vcscsvcscs/GenerationsHeritage/utilities" "github.com/vcscsvcscs/GenerationsHeritage/utilities/gin_liveness" ) @@ -23,6 +21,9 @@ var ( key = flag.String("key", "./private/keys/key.pem", "Specify the path of TLS key") httpsPort = flag.String("https", ":443", "Specify port for http secure hosting(example for format :443)") httpPort = flag.String("http", ":80", "Specify port for http hosting(example for format :80)") + memgraphURI = flag.String("memgraph", "bolt+ssc://memgraph:7687", "Specify the Memgraph database URI") + memgraphUser = flag.String("memgraph-user", "", "Specify the Memgraph database user") + memgraphPass = flag.String("memgraph-pass", "", "Specify the Memgraph database password") release = flag.Bool("release", false, "Set true to release build") logToFile = flag.Bool("log-to-file", false, "Set true to log to file") logToFileAndStd = flag.Bool("log-to-file-and-std", false, "Set true to log to file and std") @@ -34,56 +35,26 @@ func main() { if *release { gin.SetMode(gin.ReleaseMode) } - if *logToFileAndStd || *logToFile { - gin.DisableConsoleColor() // Disable Console Color, you don't need console color when writing the logs to file. - path := fmt.Sprintf("private/logs/%02dy_%02dm_%02dd_%02dh_%02dm_%02ds.log", time.Now().Year(), time.Now().Month(), time.Now().Day(), time.Now().Hour(), time.Now().Minute(), time.Now().Second()) - logerror1 := os.MkdirAll("private/logs/", 0755) - f, logerror2 := os.Create(path) - if logerror1 != nil || logerror2 != nil { - log.Println("Cant log to file") - } else if *logToFileAndStd { - gin.DefaultWriter = io.MultiWriter(f, os.Stdout) - } else { - gin.DefaultWriter = io.MultiWriter(f) - } - } - log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) - log.SetOutput(gin.DefaultErrorWriter) + + utilities.SetupLogger(*logToFileAndStd, *logToFile) hc := gin_liveness.New() + memgraphDriver := memgraph.InitDatabase(*memgraphURI, *memgraphUser, *memgraphPass) + router := gin.Default() router.GET("/health", hc.HealthCheckHandler()) + router.GET("/person", handlers.ViewPerson(memgraphDriver)) + router.POST("/person", handlers.CreatePerson(memgraphDriver)) + router.DELETE("/person", handlers.DeletePerson(memgraphDriver)) + router.PUT("/person", handlers.UpdatePerson(memgraphDriver)) + router.POST("/relationship", handlers.CreateRelationship(memgraphDriver)) + router.DELETE("/relationship", handlers.DeleteRelationship(memgraphDriver)) + router.PUT("/relationship", handlers.VerifyRelationship(memgraphDriver)) + router.POST("/createRelationshipAndPerson", handlers.CreateRelationshipAndPerson(memgraphDriver)) + router.GET("/familyTree", handlers.ViewFamiliyTree(memgraphDriver)) - var server *http.Server - - if utilities.FileExists(*cert) && utilities.FileExists(*key) { - server = &http.Server{ - Addr: *httpsPort, - Handler: router, - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - } - go func() { - log.Println("Server starts at port ", *httpsPort) - if err := server.ListenAndServeTLS(*cert, *key); err != nil && errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) - } - }() - } else { - server = &http.Server{ - Addr: *httpPort, - Handler: router, - ReadTimeout: requestTimeout * time.Second, - WriteTimeout: requestTimeout * time.Second, - } - go func() { - log.Println("Server starts at port ", *httpPort) - if err := server.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) - } - }() - } + server := utilities.SetupHttpsServer(router, *cert, *key, *httpsPort, *httpPort, requestTimeout) // Wait for interrupt signal to gracefully shutdown the server with some time to finish requests. quit := make(chan os.Signal, 1) diff --git a/backend/memgraph/create_person.go b/backend/memgraph/create_person.go new file mode 100644 index 0000000..d800efb --- /dev/null +++ b/backend/memgraph/create_person.go @@ -0,0 +1,34 @@ +package memgraph + +import ( + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "golang.org/x/net/context" +) + +func (p *Person) CreatePerson(driver neo4j.DriverWithContext) (*neo4j.Record, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) + defer session.Close(ctx) + + if err := p.Verify(); err != nil { + return nil, err + } + + p.ID = strings.ReplaceAll(uuid.New().String(), "-", "") + + query := fmt.Sprintf("CREATE (n:Person {%s}) RETURN n;", p.ToString()) + + result, err := session.Run(ctx, query, nil) + if err != nil { + return nil, err + } + + return result.Single(ctx) +} diff --git a/backend/memgraph/create_relationship.go b/backend/memgraph/create_relationship.go new file mode 100644 index 0000000..34b1d20 --- /dev/null +++ b/backend/memgraph/create_relationship.go @@ -0,0 +1,39 @@ +package memgraph + +import ( + "fmt" + "time" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "golang.org/x/net/context" +) + +func (r *Relationship) CreateRelationship(driver neo4j.DriverWithContext) (*neo4j.Record, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) + defer session.Close(ctx) + + if err := r.Verify(); err != nil { + return nil, err + } + + query := fmt.Sprintf(`MATCH (a:Person), (b:Person) WHERE a.ID = '%s' AND b.ID = '%s'`, r.FirstPersonID, r.SecondPersonID) + + if r.Direction == "->" { + query = fmt.Sprintf(`%s CREATE (a)-[r:%s {verified: false}]->(b) RETURN r;`, query, r.Relationship) + } else if r.Direction == "<-" { + query = fmt.Sprintf(`%s CREATE (a)<-[r:%s {verified: false}]-(b) RETURN r;`, query, r.Relationship) + } else { + query = fmt.Sprintf(`%s CREATE (a)<-[r1:%s {verified: True}]-(b) CREATE (a)-[r2:%s {verified: True}]->(b) RETURN r1, r2;`, + query, r.Relationship, r.Relationship) + } + + result, err := session.Run(ctx, query, nil) + if err != nil { + return nil, err + } + + return result.Single(ctx) +} diff --git a/backend/memgraph/create_relationship_and_person.go b/backend/memgraph/create_relationship_and_person.go new file mode 100644 index 0000000..ae3828f --- /dev/null +++ b/backend/memgraph/create_relationship_and_person.go @@ -0,0 +1,45 @@ +package memgraph + +import ( + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "golang.org/x/net/context" +) + +func (rp *RelationshipAndPerson) CreateRelationshipAndPerson(driver neo4j.DriverWithContext) (*neo4j.Record, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) + defer session.Close(ctx) + + if err := rp.Verify(); err != nil { + return nil, err + } + + rp.Person.ID = strings.ReplaceAll(uuid.New().String(), "-", "") + + query := fmt.Sprintf(`MATCH (a:Person) WHERE a.ID = '%s'`, rp.Relationship.FirstPersonID) + + query = fmt.Sprintf("%s CREATE (b:Person {%s})", query, rp.Person.ToString()) + + if rp.Relationship.Direction == "->" { + query = fmt.Sprintf(`%s CREATE (a)-[r:%s {verified: True}]->(b) RETURN r;`, query, rp.Relationship.Relationship) + } else if rp.Relationship.Direction == "<-" { + query = fmt.Sprintf(`%s CREATE (a)<-[r:%s {verified: True}]-(b) RETURN r;`, query, rp.Relationship.Relationship) + } else { + query = fmt.Sprintf(`%s CREATE (a)<-[r1:%s {verified: True}]-(b) CREATE (a)-[r2:%s {verified: True}]->(b) RETURN r1, r2, b;`, + query, rp.Relationship.Relationship, rp.Relationship.Relationship) + } + + result, err := session.Run(ctx, query, nil) + if err != nil { + return nil, err + } + + return result.Single(ctx) +} diff --git a/backend/memgraph/create_schema.go b/backend/memgraph/create_schema.go new file mode 100644 index 0000000..87afa46 --- /dev/null +++ b/backend/memgraph/create_schema.go @@ -0,0 +1,66 @@ +package memgraph + +import ( + "time" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "golang.org/x/net/context" +) + +const dbCreateSchemaTimeout = 10 * time.Second + +func createIndexes(driver neo4j.DriverWithContext) error { + ctx, cancel := context.WithTimeout(context.Background(), dbCreateSchemaTimeout) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) + defer session.Close(ctx) + + indexes := []string{ + `CREATE INDEX ON :Person(ID);`, + `CREATE INDEX ON :Person(Lastname);`, + `CREATE INDEX ON :Person(Firstname);`, + `CREATE INDEX ON :Person(Born);`, + `CREATE INDEX ON :Person(MothersFirstname);`, + `CREATE INDEX ON :Person(MothersLastname);`, + } + + // Run index queries via implicit auto-commit transaction + for _, index := range indexes { + _, err := session.Run(ctx, index, nil) + if err != nil { + return err + } + } + + return nil +} + +func createConstraints(driver neo4j.DriverWithContext) error { + ctx, cancel := context.WithTimeout(context.Background(), dbCreateSchemaTimeout) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) + defer session.Close(ctx) + + constraints := []string{ + `CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.ID);`, + `CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.Lastname);`, + `CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.Firstname);`, + `CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.Born);`, + `CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.MothersFirstname);`, + `CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.MothersLastname);`, + `CREATE CONSTRAINT ON (n:Person) ASSERT n.ID IS UNIQUE;`, + `CREATE CONSTRAINT ON (n:Person) ASSERT n.Lastname, n.Firstname, n.Born, n.MothersFirstname, n.MothersLastname IS UNIQUE;`, + } + + // Run index queries via implicit auto-commit transaction + for _, constraint := range constraints { + _, err := session.Run(ctx, constraint, nil) + if err != nil { + return err + } + } + + return nil +} diff --git a/backend/memgraph/cypher_verify_string.go b/backend/memgraph/cypher_verify_string.go new file mode 100644 index 0000000..af0420c --- /dev/null +++ b/backend/memgraph/cypher_verify_string.go @@ -0,0 +1,124 @@ +package memgraph + +import ( + "fmt" + "strings" +) + +var cypherKeywords = []string{ + "CREATE", + "DELETE", + "DETACH", + "DETACH DELETE", + "FOREACH", + "LOAD CSV", + "MERGE", + "MATCH", + "ON", + "OPTIONAL MATCH", + "REMOVE", + "SET", + "START", + "UNION", + "UNWIND", + "WITH", + "RETURN", + "ORDER BY", + "SKIP", + "LIMIT", + "ASC", + "DESC", + "EXISTS", + "CALL", + "USING", + "CONSTRAINT", + "DROP", + "INDEX", + "WHERE", +} + +var cypherOperators = []string{ + "+", + "-", + "*", + "/", + "%", + "^", + "=", + "<", + ">", + "<=", + ">=", + "<>", + "AND", + "OR", + "XOR", + "NOT", + "IN", + "STARTS WITH", + "ENDS WITH", + "CONTAINS", + "IS NULL", + "IS NOT NULL", + "IS UNIQUE", + "IS NODE", + "IS RELATIONSHIP", + "IS PROPERTY KEY", + "IS MAP", + "IS LIST", + "IS BOOLEAN", + "IS STRING", + "IS NUMBER", + "IS INTEGER", + "IS FLOAT", + "IS NODE", + "IS RELATIONSHIP", + "IS PATH", + "IS POINT", + "IS DATE", + "IS DURATION", +} + +// cypherDelimiters contains the delimiters that need to be escaped in a string to prevent cypher injection keys are the delimiters that need to be escaped and values are the escaped delimiters +var cypherDelimiters = map[string]string{ + "'": `\'`, + `"`: `\"`, + `\u0027`: `\\u0027`, + `\u0022`: "\\\\u0022", + "`": "``", + "\\u0060": "\\u0060\\u0060", +} + +// VerifyString verifies if a string is valid and does not contain cypher injection +func VerifyString(s string) error { + s = strings.ToUpper(s) + for _, keyword := range cypherKeywords { + if strings.Contains(s, keyword) { + return fmt.Errorf("invalid string: %s contains cypher keyword: %s", s, keyword) + } + } + + for _, operator := range cypherOperators { + if strings.Contains(s, operator) { + return fmt.Errorf("invalid string: %s contains cypher operator: %s", s, operator) + } + } + + for key := range cypherDelimiters { + if strings.Contains(s, key) { + return fmt.Errorf("invalid string: %s contains cypher delimiter: %s", s, key) + } + } + + return nil +} + +// EscapeString escapes delimiters in a string to prevent cypher injection +func EscapeString(s string) string { + result := s + for k, v := range cypherDelimiters { + result = strings.ReplaceAll(result, k, v) + } + + return result +} diff --git a/backend/memgraph/delete_person.go b/backend/memgraph/delete_person.go new file mode 100644 index 0000000..2f0cf50 --- /dev/null +++ b/backend/memgraph/delete_person.go @@ -0,0 +1,27 @@ +package memgraph + +import ( + "fmt" + "time" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "golang.org/x/net/context" +) + +func (p *Person) DeletePerson(driver neo4j.DriverWithContext) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) + defer session.Close(ctx) + + if err := p.Verify(); err != nil { + return err + } + + query := fmt.Sprintf("MATCH (n:Person {ID: '%s'}) DELETE n;", p.ID) + + _, err := session.Run(ctx, query, nil) + + return err +} diff --git a/backend/memgraph/delete_relationship.go b/backend/memgraph/delete_relationship.go new file mode 100644 index 0000000..0f50bf8 --- /dev/null +++ b/backend/memgraph/delete_relationship.go @@ -0,0 +1,36 @@ +package memgraph + +import ( + "fmt" + "time" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "golang.org/x/net/context" +) + +func (r *Relationship) DeleteRelationship(driver neo4j.DriverWithContext) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) + defer session.Close(ctx) + + if err := r.Verify(); err != nil { + return err + } + + query := "" + if r.Direction == "->" { + query = fmt.Sprintf(`MATCH (a)-[r:%s]->(b)`, r.Relationship) + } else if r.Direction == "<-" { + query = fmt.Sprintf(`MATCH (a)<-[r:%s]-(b)`, r.Relationship) + } else { + query = fmt.Sprintf(`MATCH (a)-[r:%s]-(b)`, r.Relationship) + } + + query = fmt.Sprintf(`%s WHERE a.ID = '%s' AND b.ID = '%s' DELETE r;`, query, r.FirstPersonID, r.SecondPersonID) + + _, err := session.Run(ctx, query, nil) + + return err +} diff --git a/backend/memgraph/init_database.go b/backend/memgraph/init_database.go new file mode 100644 index 0000000..9a2f545 --- /dev/null +++ b/backend/memgraph/init_database.go @@ -0,0 +1,32 @@ +package memgraph + +import ( + "context" + "log" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +func InitDatabase(dbURI, dbUser, dbPassword string) neo4j.DriverWithContext { + driver, err := neo4j.NewDriverWithContext(dbURI, neo4j.BasicAuth(dbUser, dbPassword, "")) + if err != nil { + log.Panicln(err) + } + + ctx := context.Background() + + err = driver.VerifyConnectivity(ctx) + if err != nil { + log.Panicln(err) + } + + if err := createIndexes(driver); err != nil { + log.Panicln(err) + } + + if err := createConstraints(driver); err != nil { + log.Panicln(err) + } + + return driver +} diff --git a/backend/memgraph/model.go b/backend/memgraph/model.go new file mode 100644 index 0000000..226c3dd --- /dev/null +++ b/backend/memgraph/model.go @@ -0,0 +1,272 @@ +package memgraph + +import ( + "fmt" + "time" +) + +var RelationshipTypes = []string{ + "Parent", + "Child", + "Spouse", + "Sibling", +} + +type Person struct { + ID string `json:"id"` + Firstname string `json:"first_name"` + Middlename string `json:"middle_name"` + Lastname string `json:"last_name"` + Titles []string `json:"titles"` // e.g. Jr., Sr., III + Suffixes []string `json:"suffixes"` // e.g. Ph.D., M.D. + ExtraNames []string `json:"extra_names"` + Aliases []string `json:"aliases"` + MothersFirstname string `json:"mothers_first_name"` + MothersLastname string `json:"mothers_last_name"` + Born time.Time `json:"born"` + Birthplace string `json:"birthplace"` + Residence string `json:"residence"` + Death time.Time `json:"death"` + Deathplace string `json:"deathplace"` + LifeEvents []map[string]string `json:"life_events"` + Occupations []string `json:"occupation"` + OccupationToDisplay string `json:"occupation_to_display"` + OthersSaid map[string]string `json:"others_said"` + Photos map[string]string `json:"photos"` + ProfilePicture string `json:"profile_picture"` + verified bool +} + +func (p *Person) ToString() string { + result := fmt.Sprintf("ID: '%s'", p.ID) + if p.Firstname != "" { + result = fmt.Sprintf("%s, Firstname: '%s'", result, p.Firstname) + } + if p.Lastname != "" { + result = fmt.Sprintf("%s, Lastname: '%s'", result, p.Lastname) + } + if p.Middlename != "" { + result = fmt.Sprintf("%s, Middlename: '%s'", result, p.Middlename) + } + if p.MothersFirstname != "" { + result = fmt.Sprintf("%s, MothersFirstname: '%s'", result, p.MothersFirstname) + } + if p.MothersLastname != "" { + result = fmt.Sprintf("%s, MothersLastname: '%s'", result, p.MothersLastname) + } + if !p.Born.IsZero() { + result = fmt.Sprintf("%s, Born: date({year:%d, month:%d, day:%d})", result, p.Born.Year(), p.Born.Month(), p.Born.Day()) + } + if !p.Death.IsZero() { + result = fmt.Sprintf("%s, Death: date({year:%d, month:%d, day:%d})", result, p.Death.Year(), p.Death.Month(), p.Death.Day()) + } + if p.Birthplace != "" { + result = fmt.Sprintf("%s, Birthplace: '%s'", result, p.Birthplace) + } + if p.Residence != "" { + result = fmt.Sprintf("%s, Residence: '%s'", result, p.Residence) + } + if p.Deathplace != "" { + result = fmt.Sprintf("%s, Deathplace: '%s'", result, p.Deathplace) + } + if p.OccupationToDisplay != "" { + result = fmt.Sprintf("%s, OccupationToDisplay: '%s'", result, p.OccupationToDisplay) + } + if p.ProfilePicture != "" { + result = fmt.Sprintf("%s, ProfilePicture: '%s'", result, p.ProfilePicture) + } + + if p.Titles != nil && len(p.Titles) > 0 { + result = fmt.Sprintf("%s, Titles: [", result) + for _, title := range p.Titles { + result = fmt.Sprintf("%s'%s', ", result, title) + } + result = fmt.Sprintf("%s]", result[:len(result)-2]) + } + + if p.Suffixes != nil && len(p.Suffixes) > 0 { + result = fmt.Sprintf("%s, Suffixes: [", result) + for _, suffix := range p.Suffixes { + result = fmt.Sprintf("%s'%s', ", result, suffix) + } + result = fmt.Sprintf("%s]", result[:len(result)-2]) + } + + if p.ExtraNames != nil && len(p.ExtraNames) > 0 { + result = fmt.Sprintf("%s, ExtraNames: [", result) + for _, extraName := range p.ExtraNames { + result = fmt.Sprintf("%s'%s', ", result, extraName) + } + result = fmt.Sprintf("%s]", result[:len(result)-2]) + } + + if p.Aliases != nil && len(p.Aliases) > 0 { + result = fmt.Sprintf("%s, Aliases: [", result) + for _, alias := range p.Aliases { + result = fmt.Sprintf("%s'%s', ", result, alias) + } + result = fmt.Sprintf("%s]", result[:len(result)-2]) + } + + if p.LifeEvents != nil && len(p.LifeEvents) > 0 { + result = fmt.Sprintf("%s, LifeEvents: [", result) + for i := 0; i < len(p.LifeEvents); i++ { + date, dok := p.LifeEvents[i]["date"] + event, eok := p.LifeEvents[i]["event"] + if dok && eok { + result = fmt.Sprintf("%s{date: '%s', event: '%s'}, ", result, date, event) + } + } + result = fmt.Sprintf("%s]", result[:len(result)-2]) + } + + if p.Occupations != nil && len(p.Occupations) > 0 { + result = fmt.Sprintf("%s, Occupations: [", result) + + for _, occupation := range p.Occupations { + result = fmt.Sprintf("%s'%s', ", result, occupation) + } + + result = fmt.Sprintf("%s]", result[:len(result)-2]) + } + + if p.OthersSaid != nil { + result = fmt.Sprintf("%s, OthersSaid: {", result) + for key, value := range p.OthersSaid { + result = fmt.Sprintf("%s%s: '%s', ", result, key, value) + } + result = fmt.Sprintf("%s}", result[:len(result)-2]) + } + + if p.Photos != nil && len(p.Photos) > 0 { + result = fmt.Sprintf("%s, Photos: {", result) + for key, value := range p.Photos { + result = fmt.Sprintf("%s%s: '%s', ", result, key, value) + } + result = fmt.Sprintf("%s}", result[:len(result)-2]) + } + + return result +} + +// Verify checks if the person is valid and does not contain cypher injection it also escapes the delimiters contained in any of the strings +func (p *Person) Verify() error { + if p.verified { + return nil + } + if err := VerifyString(p.ID); err != nil { + return fmt.Errorf("invalid ID type %s", err) + } + + p.Firstname = EscapeString(p.Firstname) + p.Middlename = EscapeString(p.Middlename) + p.Lastname = EscapeString(p.Lastname) + p.MothersFirstname = EscapeString(p.MothersFirstname) + p.MothersLastname = EscapeString(p.MothersLastname) + p.Birthplace = EscapeString(p.Birthplace) + p.Residence = EscapeString(p.Residence) + p.Deathplace = EscapeString(p.Deathplace) + p.OccupationToDisplay = EscapeString(p.OccupationToDisplay) + p.ProfilePicture = EscapeString(p.ProfilePicture) + + for i, title := range p.Titles { + p.Titles[i] = EscapeString(title) + } + + for i, suffix := range p.Suffixes { + p.Suffixes[i] = EscapeString(suffix) + } + + for i, extraName := range p.ExtraNames { + p.ExtraNames[i] = EscapeString(extraName) + } + + for i, alias := range p.Aliases { + p.Aliases[i] = EscapeString(alias) + } + + for i, lifeEvent := range p.LifeEvents { + for key, value := range lifeEvent { + if key != "date" && key != "event" { + return fmt.Errorf("invalid key in life event") + } + p.LifeEvents[i][key] = EscapeString(value) + } + } + + for i, occupation := range p.Occupations { + p.Occupations[i] = EscapeString(occupation) + } + + for key, value := range p.OthersSaid { + if err := VerifyString(key); err != nil { + return fmt.Errorf("invalid key in others said %s", err) + } + p.OthersSaid[key] = EscapeString(value) + } + + for key, value := range p.Photos { + if err := VerifyString(key); err != nil { + return fmt.Errorf("invalid key in photos %s", err) + } + p.Photos[key] = EscapeString(value) + } + + p.verified = true + + return nil +} + +type Relationship struct { + FirstPersonID string `json:"first_person_id"` + SecondPersonID string `json:"second_person_id"` + Relationship string `json:"relationship"` + Direction string `json:"direction"` +} + +// Verify checks if the relationship is valid and does not contain cypher injection +func (r *Relationship) Verify() error { + if r.Direction != "->" && r.Direction != "<-" && r.Direction != "-" { + return fmt.Errorf("invalid direction for relationship") + } + + // Check if the relationship is in the list of valid relationships + found := false + for _, relationship := range RelationshipTypes { + if r.Relationship == relationship { + found = true + + break + } + } + if !found { + return fmt.Errorf("invalid relationship type") + } + + if err := VerifyString(r.FirstPersonID); err != nil { + return fmt.Errorf("invalid FirstPersonID type %s", err) + } + + if err := VerifyString(r.SecondPersonID); err != nil { + return fmt.Errorf("invalid SecondPersonID type %s", err) + } + + return nil +} + +type RelationshipAndPerson struct { + Relationship Relationship `json:"relationship"` + Person Person `json:"person"` +} + +func (r *RelationshipAndPerson) Verify() error { + if err := r.Relationship.Verify(); err != nil { + return err + } + + if err := r.Person.Verify(); err != nil { + return err + } + + return nil +} diff --git a/backend/memgraph/model_test.go b/backend/memgraph/model_test.go new file mode 100644 index 0000000..0b47c0a --- /dev/null +++ b/backend/memgraph/model_test.go @@ -0,0 +1,77 @@ +package memgraph + +import ( + "testing" + "time" +) + +func TestPerson_ToString(t *testing.T) { + tests := []struct { + name string + p *Person + want string + }{ + { + name: "Test with nil values", + p: &Person{ + ID: "1", + Firstname: "John", + Lastname: "Doe", + MothersFirstname: "Jane", + MothersLastname: "Doe", + Born: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Birthplace: "New York", + Residence: "New York", + Death: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Deathplace: "New York", + }, + want: "ID: '1', Firstname: 'John', Lastname: 'Doe', MothersFirstname: 'Jane', MothersLastname: 'Doe', Born: date({year:2021, month:1, day:1}), Death: date({year:2021, month:1, day:1}), Birthplace: 'New York', Residence: 'New York', Deathplace: 'New York', OccupationToDisplay: '', ProfilePicture: ''", + }, { + name: "Test with All values", + p: &Person{ + ID: "1", + Firstname: "John", + Lastname: "Doe", + MothersFirstname: "Jane", + MothersLastname: "Doe", + Born: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Birthplace: "New York", + Residence: "New York", + Death: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Deathplace: "New York", + LifeEvents: []map[string]string{ + { + "date": "2021-01-01", + "event": "Event 1", + }, + { + "date": "2021-01-02", + "event": "Event 2", + }, + }, + Occupations: []string{ + "Welder", + "Plumber", + }, + OccupationToDisplay: "Welder", + OthersSaid: map[string]string{ + "Beni": "He is a good person", + "Jani": "He is a bad person", + }, + Photos: map[string]string{ + "Profile": "profile.jpg", + "Family": "family.jpg", + }, + ProfilePicture: "profile.jpg", + }, + want: "ID: '1', Firstname: 'John', Lastname: 'Doe', MothersFirstname: 'Jane', MothersLastname: 'Doe', Born: date({year:2021, month:1, day:1}), Death: date({year:2021, month:1, day:1}), Birthplace: 'New York', Residence: 'New York', Deathplace: 'New York', OccupationToDisplay: 'Welder', ProfilePicture: 'profile.jpg', LifeEvents: [{date: '2021-01-01', event: 'Event 1'}, {date: '2021-01-02', event: 'Event 2'}], Occupations: ['Welder', 'Plumber'], OthersSaid: {Beni: 'He is a good person', Jani: 'He is a bad person'}, Photos: {Profile: 'profile.jpg', Family: 'family.jpg'}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.p.ToString(); got != tt.want { + t.Errorf("Person.ToString() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/backend/memgraph/update_person.go b/backend/memgraph/update_person.go new file mode 100644 index 0000000..27dcba8 --- /dev/null +++ b/backend/memgraph/update_person.go @@ -0,0 +1,30 @@ +package memgraph + +import ( + "fmt" + "time" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "golang.org/x/net/context" +) + +func (p *Person) UpdatePerson(driver neo4j.DriverWithContext) (*neo4j.Record, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) + defer session.Close(ctx) + + if err := p.Verify(); err != nil { + return nil, err + } + + query := fmt.Sprintf("MATCH (n:Person {ID: '%s'}) SET n += {%s} RETURN n;", p.ID, p.ToString()) + + result, err := session.Run(ctx, query, nil) + if err != nil { + return nil, err + } + + return result.Single(ctx) +} diff --git a/backend/memgraph/verify_relationship.go b/backend/memgraph/verify_relationship.go new file mode 100644 index 0000000..5e302af --- /dev/null +++ b/backend/memgraph/verify_relationship.go @@ -0,0 +1,39 @@ +package memgraph + +import ( + "fmt" + "time" + + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "golang.org/x/net/context" +) + +func (r *Relationship) VerifyRelationship(driver neo4j.DriverWithContext) (*neo4j.Record, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}) + defer session.Close(ctx) + + if err := r.Verify(); err != nil { + return nil, err + } + + query := "" + if r.Direction == "->" { + query = fmt.Sprintf(`MATCH (a)-[r:%s]->(b)`, r.Relationship) + } else if r.Direction == "<-" { + query = fmt.Sprintf(`MATCH (a)<-[r:%s]-(b)`, r.Relationship) + } else { + query = fmt.Sprintf(`MATCH (a)-[r:%s]-(b)`, r.Relationship) + } + + query = fmt.Sprintf(`%s WHERE a.ID = %s AND b.ID = %s set r.verified = true return r;`, query, r.FirstPersonID, r.SecondPersonID) + + result, err := session.Run(ctx, query, nil) + if err != nil { + return nil, err + } + + return result.Single(ctx) +}