Merge branch 'feature/user-profile' of Cadoles/daddy into develop
This commit is contained in:
commit
0cb6c7c67e
@ -4,4 +4,4 @@ OIDC_CLIENT_SECRET=daddycool
|
||||
OIDC_POST_LOGOUT_REDIRECT_URL=http://localhost:8081/logout/redirect
|
||||
HTTP_COOKIE_AUTHENTICATION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ
|
||||
HTTP_COOKIE_ENCRYPTION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ
|
||||
DATABASE_DSN="host=localhost user=daddy database=daddy password=daddy"
|
||||
DATABASE_DSN="host=localhost user=daddy database=daddy password=daddy port=5432 sslmode=disable"
|
2
Makefile
2
Makefile
@ -6,7 +6,7 @@ docker:
|
||||
docker-compose build
|
||||
|
||||
generate:
|
||||
cd internal && go run github.com/99designs/gqlgen generate
|
||||
go generate ./...
|
||||
|
||||
build-server:
|
||||
CGO_ENABLED=0 go build -v -o ./bin/server ./cmd/server
|
||||
|
@ -29,7 +29,7 @@ Les services suivants devraient être disponibles après démarrage de l'environ
|
||||
|Service|Type|Accès|Description|
|
||||
|-------|----|-----|-----------|
|
||||
|Application React|HTTP (UI)|http://localhost:8080/|Page d'accueil de l'application React (serveur Webpack)|
|
||||
|Interface Web GraphQL|HTTP (UI)|http://localhost:8081/api/v1/graphql (GET)|Interface Web de développement de l'API GraphQL (mode debug uniquement, nécessite d'être authentifié)|
|
||||
|Interface Web GraphQL|HTTP (UI)|http://localhost:8081/api/v1/playground|Interface Web de développement de l'API GraphQL (mode debug uniquement, nécessite d'être authentifié)|
|
||||
|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8081/api/v1/graphql (POST)|Point d'entrée de l'API GraphQL|
|
||||
|Serveur Hydra|HTTP (ReST)|http://localhost:4444|Point d'entrée pour l'API OAuth2 d'[Hydra](https://www.ory.sh/hydra/docs/)|
|
||||
|Serveur Hydra Passwordless|HTTP|http://localhost:3000|Point d'entrée pour la ["Login/Consent App"](https://www.ory.sh/hydra/docs/implementing-consent) [hydra-passwordless](https://forge.cadoles.com/wpetit/hydra-passwordless)|
|
||||
|
198
client/package-lock.json
generated
198
client/package-lock.json
generated
@ -1230,8 +1230,7 @@
|
||||
"@types/node": {
|
||||
"version": "13.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.12.tgz",
|
||||
"integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw=="
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
@ -1372,6 +1371,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/zen-observable": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz",
|
||||
"integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg=="
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
|
||||
@ -1547,6 +1551,23 @@
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"@wry/context": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz",
|
||||
"integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==",
|
||||
"requires": {
|
||||
"@types/node": ">=6",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@wry/equality": {
|
||||
"version": "0.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz",
|
||||
"integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
@ -1723,6 +1744,93 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apollo-cache": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.3.5.tgz",
|
||||
"integrity": "sha512-1XoDy8kJnyWY/i/+gLTEbYLnoiVtS8y7ikBr/IfmML4Qb+CM7dEEbIUOjnY716WqmZ/UpXIxTfJsY7rMcqiCXA==",
|
||||
"requires": {
|
||||
"apollo-utilities": "^1.3.4",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"apollo-cache-inmemory": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.6.tgz",
|
||||
"integrity": "sha512-L8pToTW/+Xru2FFAhkZ1OA9q4V4nuvfoPecBM34DecAugUZEBhI2Hmpgnzq2hTKZ60LAMrlqiASm0aqAY6F8/A==",
|
||||
"requires": {
|
||||
"apollo-cache": "^1.3.5",
|
||||
"apollo-utilities": "^1.3.4",
|
||||
"optimism": "^0.10.0",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"apollo-client": {
|
||||
"version": "2.6.10",
|
||||
"resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.6.10.tgz",
|
||||
"integrity": "sha512-jiPlMTN6/5CjZpJOkGeUV0mb4zxx33uXWdj/xQCfAMkuNAC3HN7CvYDyMHHEzmcQ5GV12LszWoQ/VlxET24CtA==",
|
||||
"requires": {
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"apollo-cache": "1.3.5",
|
||||
"apollo-link": "^1.0.0",
|
||||
"apollo-utilities": "1.3.4",
|
||||
"symbol-observable": "^1.0.2",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.10.0",
|
||||
"zen-observable": "^0.8.0"
|
||||
}
|
||||
},
|
||||
"apollo-link": {
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz",
|
||||
"integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==",
|
||||
"requires": {
|
||||
"apollo-utilities": "^1.3.0",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.9.3",
|
||||
"zen-observable-ts": "^0.8.21"
|
||||
}
|
||||
},
|
||||
"apollo-link-http": {
|
||||
"version": "1.5.17",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz",
|
||||
"integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==",
|
||||
"requires": {
|
||||
"apollo-link": "^1.2.14",
|
||||
"apollo-link-http-common": "^0.2.16",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"apollo-link-http-common": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz",
|
||||
"integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==",
|
||||
"requires": {
|
||||
"apollo-link": "^1.2.14",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"apollo-link-ws": {
|
||||
"version": "1.0.20",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link-ws/-/apollo-link-ws-1.0.20.tgz",
|
||||
"integrity": "sha512-mjSFPlQxmoLArpHBeUb2Xj+2HDYeTaJqFGOqQ+I8NVJxgL9lJe84PDWcPah/yMLv3rB7QgBDSuZ0xoRFBPlySw==",
|
||||
"requires": {
|
||||
"apollo-link": "^1.2.14",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"apollo-utilities": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz",
|
||||
"integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==",
|
||||
"requires": {
|
||||
"@wry/equality": "^0.1.2",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
@ -1875,8 +1983,7 @@
|
||||
"async-limiter": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
||||
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
@ -2724,6 +2831,11 @@
|
||||
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
|
||||
"dev": true
|
||||
},
|
||||
"backo2": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
|
||||
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
@ -5029,8 +5141,7 @@
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||
},
|
||||
"fastparse": {
|
||||
"version": "1.1.2",
|
||||
@ -5499,11 +5610,21 @@
|
||||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
|
||||
"dev": true
|
||||
},
|
||||
"graphql": {
|
||||
"version": "15.3.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz",
|
||||
"integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="
|
||||
},
|
||||
"graphql-request": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-2.0.0.tgz",
|
||||
"integrity": "sha512-Ww3Ax+G3l2d+mPT8w7HC9LfrKjutnCKtnDq7ZZp2ghVk5IQDjwAk3/arRF1ix17Ky15rm0hrSKVKxRhIVlSuoQ=="
|
||||
},
|
||||
"graphql-tag": {
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.4.tgz",
|
||||
"integrity": "sha512-O7vG5BT3w6Sotc26ybcvLKNTdfr4GfsIVMD+LdYqXCeJIYPRyp8BIsDOUtxw7S1PYvRw5vH3278J2EDezR6mfA=="
|
||||
},
|
||||
"handle-thing": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
|
||||
@ -6308,6 +6429,11 @@
|
||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
|
||||
"dev": true
|
||||
},
|
||||
"iterall": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz",
|
||||
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg=="
|
||||
},
|
||||
"js-base64": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz",
|
||||
@ -7340,6 +7466,14 @@
|
||||
"is-wsl": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"optimism": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz",
|
||||
"integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==",
|
||||
"requires": {
|
||||
"@wry/context": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"original": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
|
||||
@ -9420,6 +9554,33 @@
|
||||
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
|
||||
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
|
||||
},
|
||||
"subscriptions-transport-ws": {
|
||||
"version": "0.9.17",
|
||||
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.17.tgz",
|
||||
"integrity": "sha512-hNHi2N80PBz4T0V0QhnnsMGvG3XDFDS9mS6BhZ3R12T6EBywC8d/uJscsga0cVO4DKtXCkCRrWm2sOYrbOdhEA==",
|
||||
"requires": {
|
||||
"backo2": "^1.0.2",
|
||||
"eventemitter3": "^3.1.0",
|
||||
"iterall": "^1.2.1",
|
||||
"symbol-observable": "^1.0.4",
|
||||
"ws": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"eventemitter3": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
|
||||
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
|
||||
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
|
||||
"requires": {
|
||||
"async-limiter": "~1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
@ -9658,6 +9819,14 @@
|
||||
"glob": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"ts-invariant": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz",
|
||||
"integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"ts-loader": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz",
|
||||
@ -9725,8 +9894,7 @@
|
||||
"tslib": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
|
||||
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
|
||||
},
|
||||
"tty-browserify": {
|
||||
"version": "0.0.0",
|
||||
@ -10833,6 +11001,20 @@
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"zen-observable": {
|
||||
"version": "0.8.15",
|
||||
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
|
||||
"integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="
|
||||
},
|
||||
"zen-observable-ts": {
|
||||
"version": "0.8.21",
|
||||
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz",
|
||||
"integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.3",
|
||||
"zen-observable": "^0.8.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,9 +52,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.3",
|
||||
"apollo-cache-inmemory": "^1.6.6",
|
||||
"apollo-client": "^2.6.10",
|
||||
"apollo-link": "^1.2.14",
|
||||
"apollo-link-http": "^1.5.17",
|
||||
"apollo-link-ws": "^1.0.20",
|
||||
"apollo-utilities": "^1.3.4",
|
||||
"bulma": "^0.7.2",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-request": "^2.0.0",
|
||||
"graphql-tag": "^2.10.4",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^16.12.0",
|
||||
@ -65,6 +73,7 @@
|
||||
"redux": "^4.0.4",
|
||||
"redux-saga": "^1.1.3",
|
||||
"styled-components": "^4.4.1",
|
||||
"subscriptions-transport-ws": "^0.9.17",
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
|
||||
import { HomePage } from './HomePage/HomePage';
|
||||
import { store } from '../store/store';
|
||||
import { Provider } from 'react-redux';
|
||||
import { ProfilePage } from './ProfilePage/ProfilePage';
|
||||
|
||||
export class App extends React.Component {
|
||||
render() {
|
||||
@ -11,6 +12,7 @@ export class App extends React.Component {
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/profile" exact component={ProfilePage} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
|
@ -7,15 +7,15 @@ export function HomePage() {
|
||||
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
|
||||
|
||||
return (
|
||||
<Page title="Daddy - Accueil">
|
||||
<Page title="Accueil">
|
||||
<div className="container is-fluid">
|
||||
<section className="section">
|
||||
<div className="columns">
|
||||
<div className="column is-4 is-offset-4">
|
||||
<div className="box">
|
||||
{
|
||||
currentUser && currentUser.full_name ?
|
||||
<p>Bonjour <span className="has-text-weight-bold">{currentUser.full_name}</span> !</p> :
|
||||
currentUser && currentUser.email ?
|
||||
<p>Bonjour <span className="has-text-weight-bold">{currentUser.name ? currentUser.name : currentUser.email}</span> !</p> :
|
||||
<p>Veuillez vous authentifier.</p>
|
||||
}
|
||||
</div>
|
||||
|
@ -1,44 +1,62 @@
|
||||
import React from 'react';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import logo from '../resources/logo.svg';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../store/reducers/root';
|
||||
import { Config } from '../config';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export function Navbar() {
|
||||
const isAuthenticated = useSelector<RootState>(state => state.auth.isAuthenticated);
|
||||
|
||||
const [ isActive, setActive ] = useState(false);
|
||||
|
||||
const toggleMenu = () => {
|
||||
setActive(active => !active);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||
<div className="container is-fluid">
|
||||
<div className="navbar-brand">
|
||||
<a className="navbar-item" href="#/">
|
||||
<Link className="navbar-item" to="/">
|
||||
<img src={logo} style={{marginRight:'5px',width:'28px',height:'28px'}} />
|
||||
<h1 className="is-size-4">Daddy</h1>
|
||||
</a>
|
||||
<a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
</Link>
|
||||
<a role="button"
|
||||
className={`navbar-burger ${isActive ? 'is-active' : ''}`}
|
||||
onClick={toggleMenu}
|
||||
aria-label="menu"
|
||||
aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="navbar-menu">
|
||||
<div className={`navbar-menu ${isActive ? 'is-active' : ''}`}>
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
<div className="buttons">
|
||||
{
|
||||
isAuthenticated ?
|
||||
<a className="button is-small" href={Config.logoutURL}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
<span>Se déconnecter</span>
|
||||
</a> :
|
||||
<a className="button is-small" href={Config.loginURL}>
|
||||
<Fragment>
|
||||
<Link to="/profile" className="button">
|
||||
<span className="icon">
|
||||
<i className="fas fa-user"></i>
|
||||
</span>
|
||||
</Link>
|
||||
<a className="button" href={Config.logoutURL}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</Fragment> :
|
||||
<a className="button" href={Config.loginURL}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-in-alt"></i>
|
||||
</span>
|
||||
<span>Se connecter</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,6 +25,6 @@ export class Page extends React.PureComponent<PageProps> {
|
||||
|
||||
updateTitle() {
|
||||
const { title } = this.props;
|
||||
if (title !== undefined) window.document.title = title;
|
||||
if (title !== undefined) window.document.title = title + ' - Daddy';
|
||||
}
|
||||
}
|
38
client/src/components/ProfilePage/ProfilePage.tsx
Normal file
38
client/src/components/ProfilePage/ProfilePage.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { RootState } from '../../store/reducers/root';
|
||||
import { UserForm } from '../UserForm';
|
||||
import { Loader } from '../Loader';
|
||||
import { User } from '../../types/user';
|
||||
import { updateProfile } from '../../store/actions/profile';
|
||||
|
||||
export function ProfilePage() {
|
||||
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onUserChange = (user: User) => {
|
||||
if (currentUser.name !== user.name) {
|
||||
dispatch(updateProfile({ name: user.name }))
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page title="Mon profil">
|
||||
<div className="container is-fluid">
|
||||
<section className="section">
|
||||
<div className="columns">
|
||||
<div className="column is-6 is-offset-3">
|
||||
<h2 className="is-size-2 subtitle">Mon profil</h2>
|
||||
{
|
||||
currentUser ?
|
||||
<UserForm onChange={onUserChange} user={currentUser} /> :
|
||||
<Loader />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
80
client/src/components/UserForm.tsx
Normal file
80
client/src/components/UserForm.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { User } from '../types/user';
|
||||
|
||||
export interface UserFormProps {
|
||||
user: User
|
||||
onChange?: (user: User) => void
|
||||
}
|
||||
|
||||
export function UserForm({ user, onChange }: UserFormProps) {
|
||||
const [ state, setState ] = useState({
|
||||
changed: false,
|
||||
user: {
|
||||
name: '',
|
||||
email: '',
|
||||
createdAt: null,
|
||||
connectedAt: null,
|
||||
...user,
|
||||
}
|
||||
});
|
||||
|
||||
const onSaveClick = () => {
|
||||
if (!state.changed) return;
|
||||
if (typeof onChange !== 'function') return;
|
||||
onChange(state.user);
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
changed: false,
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const onUserAttrChange = function(attrName: string, evt: ChangeEvent<HTMLInputElement>) {
|
||||
const value = evt.currentTarget.value;
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
changed: true,
|
||||
user: {
|
||||
...state.user,
|
||||
[attrName]: value,
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form">
|
||||
<div className="field">
|
||||
<label className="label">Nom d'utilisateur</label>
|
||||
<div className="control">
|
||||
<input type="text" className="input" value={state.user.name}
|
||||
onChange={onUserAttrChange.bind(null, "name")} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Adresse courriel</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Date de dernière connexion</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.user.connectedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Date de création</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.user.createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="buttons is-right">
|
||||
<button disabled={!state.changed}
|
||||
className="button is-primary" onClick={onSaveClick}>Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -2,6 +2,7 @@ export const Config = {
|
||||
loginURL: get<string>("loginURL", "http://localhost:8081/login"),
|
||||
logoutURL: get<string>("logoutURL", "http://localhost:8081/logout"),
|
||||
graphQLEndpoint: get<string>("graphQLEndpoint", "http://localhost:8081/api/v1/graphql"),
|
||||
subscriptionEndpoint: get<string>("subscriptionEndpoint", "ws://localhost:8081/api/v1/graphql"),
|
||||
};
|
||||
|
||||
function get<T>(key: string, defaultValue: T):T {
|
||||
|
@ -16,4 +16,25 @@ export interface fetchProfileSuccessAction extends Action {
|
||||
|
||||
export function fetchProfile(): fetchProfileRequestAction {
|
||||
return { type: FETCH_PROFILE_REQUEST }
|
||||
}
|
||||
|
||||
export const UPDATE_PROFILE_REQUEST = 'UPDATE_PROFILE_REQUEST';
|
||||
export const UPDATE_PROFILE_SUCCESS = 'UPDATE_PROFILE_SUCCESS';
|
||||
export const UPDATE_PROFILE_FAILURE = 'UPDATE_PROFILE_FAILURE';
|
||||
|
||||
export interface ProfileChanges {
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface updateProfileRequestAction extends Action {
|
||||
changes: ProfileChanges
|
||||
}
|
||||
|
||||
export interface updateProfileSuccessAction extends Action {
|
||||
profile: User
|
||||
}
|
||||
|
||||
|
||||
export function updateProfile(changes: ProfileChanges): updateProfileRequestAction {
|
||||
return { type: UPDATE_PROFILE_REQUEST, changes }
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Action } from "redux";
|
||||
import { User } from "../../types/user";
|
||||
import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth";
|
||||
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } from "../actions/profile";
|
||||
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction, updateProfileSuccessAction, UPDATE_PROFILE_SUCCESS, updateProfileRequestAction } from "../actions/profile";
|
||||
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean
|
||||
@ -19,6 +19,8 @@ export function authReducer(state = defaultState, action: Action): AuthState {
|
||||
return handleSetCurrentUser(state, action as setCurrentUserAction);
|
||||
case FETCH_PROFILE_SUCCESS:
|
||||
return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction);
|
||||
case UPDATE_PROFILE_SUCCESS:
|
||||
return handleFetchProfileSuccess(state, action as updateProfileSuccessAction);
|
||||
|
||||
}
|
||||
return state;
|
||||
@ -42,4 +44,14 @@ function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSu
|
||||
...profile,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function handleUpdateProfileSuccess(state: AuthState, { profile }: updateProfileSuccessAction): AuthState {
|
||||
return {
|
||||
...state,
|
||||
isAuthenticated: true,
|
||||
currentUser: {
|
||||
...profile,
|
||||
}
|
||||
};
|
||||
};
|
@ -1,7 +1,12 @@
|
||||
import { all, put } from "redux-saga/effects";
|
||||
import { fetchProfile } from "../actions/profile";
|
||||
|
||||
export function* initRootSaga() {
|
||||
yield all([
|
||||
|
||||
fetchUserProfileSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* fetchUserProfileSaga() {
|
||||
yield put(fetchProfile());
|
||||
}
|
46
client/src/store/sagas/profile.ts
Normal file
46
client/src/store/sagas/profile.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { DaddyClient, getClient } from "../../util/daddy";
|
||||
import { Config } from "../../config";
|
||||
import { all, takeLatest, put, select } from "redux-saga/effects";
|
||||
import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS, updateProfileRequestAction, UPDATE_PROFILE_REQUEST, UPDATE_PROFILE_FAILURE, UPDATE_PROFILE_SUCCESS } from "../actions/profile";
|
||||
import { SET_CURRENT_USER } from "../actions/auth";
|
||||
import { User } from "../../types/user";
|
||||
|
||||
export function* profileRootSaga() {
|
||||
yield all([
|
||||
takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga),
|
||||
takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga),
|
||||
takeLatest(UPDATE_PROFILE_REQUEST, updateProfileSaga),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* onCurrentUserChangeSaga() {
|
||||
yield put(fetchProfile());
|
||||
}
|
||||
|
||||
export function* fetchProfileSaga() {
|
||||
const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint);
|
||||
|
||||
let profile: User;
|
||||
try {
|
||||
profile = yield client.fetchProfile().then(result => result.userProfile);
|
||||
} catch(err) {
|
||||
yield put({ type: FETCH_PROFILE_FAILURE, err });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({type: FETCH_PROFILE_SUCCESS, profile });
|
||||
}
|
||||
|
||||
export function* updateProfileSaga({ changes }: updateProfileRequestAction) {
|
||||
const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint);
|
||||
|
||||
let profile: User;
|
||||
try {
|
||||
profile = yield client.updateProfile(changes).then(result => result.updateProfile);
|
||||
} catch(err) {
|
||||
yield put({ type: UPDATE_PROFILE_FAILURE, err });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({type: UPDATE_PROFILE_SUCCESS, profile });
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
import { failureRootSaga } from './failure';
|
||||
import { initRootSaga } from './init';
|
||||
import { usersRootSaga } from './users';
|
||||
import { profileRootSaga } from './profile';
|
||||
|
||||
export function* rootSaga() {
|
||||
yield all([
|
||||
initRootSaga(),
|
||||
failureRootSaga(),
|
||||
profileRootSaga(),
|
||||
]);
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
import { DaddyClient } from "../../util/daddy";
|
||||
import { Config } from "../../config";
|
||||
import { all, takeLatest, put, select } from "redux-saga/effects";
|
||||
import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from "../actions/profile";
|
||||
import { SET_CURRENT_USER } from "../actions/auth";
|
||||
import { RootState } from "../reducers/root";
|
||||
import { User } from "../../types/user";
|
||||
|
||||
export function* usersRootSaga() {
|
||||
yield all([
|
||||
takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga),
|
||||
takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* onCurrentUserChangeSaga() {
|
||||
yield put(fetchProfile());
|
||||
}
|
||||
|
||||
export function* fetchProfileSaga() {
|
||||
const client = new DaddyClient(Config.graphQLEndpoint);
|
||||
|
||||
let profile: User;
|
||||
try {
|
||||
const currentUser: User = yield select((state: RootState) => state.auth.currentUser);
|
||||
profile = yield client.fetchUser(currentUser.email).then(result => result.user);
|
||||
} catch(err) {
|
||||
yield put({ type: FETCH_PROFILE_FAILURE, err });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({type: FETCH_PROFILE_SUCCESS, profile });
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
export interface User {
|
||||
email: string
|
||||
full_name?: string
|
||||
updated_at?: Date
|
||||
created_at?: Date
|
||||
name?: string
|
||||
connectedAt?: Date
|
||||
createdAt?: Date
|
||||
}
|
@ -1,5 +1,11 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { Config } from "../config";
|
||||
import ApolloClient, { DefaultOptions } from 'apollo-client';
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import { split } from 'apollo-link';
|
||||
import { HttpLink } from 'apollo-link-http';
|
||||
import { WebSocketLink } from 'apollo-link-ws';
|
||||
import { getMainDefinition, variablesInOperation } from 'apollo-utilities';
|
||||
import gql from 'graphql-tag';
|
||||
import { ProfileChanges } from '../store/actions/profile';
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor(...args: any[]) {
|
||||
@ -8,30 +14,96 @@ export class UnauthorizedError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
let client: DaddyClient
|
||||
|
||||
export function getClient(graphQLEndpoint: string, subscriptionEndpoint: string): DaddyClient {
|
||||
if (!client) {
|
||||
client = new DaddyClient(graphQLEndpoint, subscriptionEndpoint);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export class DaddyClient {
|
||||
|
||||
gql: GraphQLClient
|
||||
gql: ApolloClient<InMemoryCache>
|
||||
|
||||
constructor(endpoint: string) {
|
||||
this.gql = new GraphQLClient(endpoint, {
|
||||
headers: {
|
||||
mode: 'cors',
|
||||
constructor(graphQLEndpoint: string, subscriptionEndpoint: string) {
|
||||
const wsLink = new WebSocketLink({
|
||||
uri: subscriptionEndpoint,
|
||||
options: {
|
||||
reconnect: true
|
||||
}
|
||||
});
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: graphQLEndpoint,
|
||||
fetchOptions: {
|
||||
mode: 'cors',
|
||||
credentials: 'include',
|
||||
}
|
||||
});
|
||||
|
||||
const link = split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return (
|
||||
definition.kind === 'OperationDefinition' &&
|
||||
definition.operation === 'subscription'
|
||||
);
|
||||
},
|
||||
wsLink,
|
||||
httpLink,
|
||||
);
|
||||
|
||||
const defaultOptions: DefaultOptions = {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'no-cache',
|
||||
errorPolicy: 'ignore',
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: 'no-cache',
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
};
|
||||
|
||||
this.gql = new ApolloClient<any>({
|
||||
link: link,
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions,
|
||||
});
|
||||
}
|
||||
|
||||
fetchUser(email: string) {
|
||||
return this.gql.rawRequest(`
|
||||
query fetchUser {
|
||||
user(where: {email: {eq: $email}}) {
|
||||
id
|
||||
created_at
|
||||
updated_at
|
||||
fetchProfile() {
|
||||
return this.gql.query({
|
||||
query: gql`
|
||||
query {
|
||||
userProfile {
|
||||
name,
|
||||
email,
|
||||
full_name
|
||||
createdAt,
|
||||
connectedAt
|
||||
}
|
||||
}
|
||||
`, { email })
|
||||
}`
|
||||
})
|
||||
.then(this.assertAuthorization)
|
||||
}
|
||||
|
||||
updateProfile(changes: ProfileChanges) {
|
||||
return this.gql.mutate({
|
||||
variables: {
|
||||
changes,
|
||||
},
|
||||
mutation: gql`
|
||||
mutation updateProfile($changes: ProfileChanges!) {
|
||||
updateProfile(changes: $changes) {
|
||||
name,
|
||||
email,
|
||||
createdAt,
|
||||
connectedAt
|
||||
}
|
||||
}`,
|
||||
})
|
||||
.then(this.assertAuthorization)
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/migration"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/database"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
@ -84,21 +81,7 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con
|
||||
oidc.WithScopes("email", "openid"),
|
||||
))
|
||||
|
||||
ctn.Provide(database.ServiceName, database.ServiceProvider(conf.Database.DSN))
|
||||
|
||||
dbpool, err := database.From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve database service")
|
||||
}
|
||||
|
||||
versionResolver := database.NewVersionResolver(dbpool)
|
||||
if err := versionResolver.Init(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "could not initialize database version resolver")
|
||||
}
|
||||
|
||||
ctn.Provide(migration.ServiceName, migration.ServiceProvider(versionResolver))
|
||||
|
||||
ctn.Provide(cqrs.ServiceName, cqrs.ServiceProvider())
|
||||
ctn.Provide(orm.ServiceName, orm.ServiceProvider("postgres", conf.Database.DSN, conf.Debug))
|
||||
|
||||
return ctn, nil
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/command"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/query"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
func initCommands(ctn *service.Container) error {
|
||||
dispatcher, err := cqrs.From(ctn)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
dispatcher.RegisterCommand(
|
||||
cqrs.MatchCommandRequest(&command.CreateUserCommandRequest{}),
|
||||
cqrs.CommandHandlerFunc(command.HandleCreateUserCommand),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initQueries(ctn *service.Container) error {
|
||||
dispatcher, err := cqrs.From(ctn)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
dispatcher.RegisterQuery(
|
||||
cqrs.MatchQueryRequest(&query.FindUserQueryRequest{}),
|
||||
cqrs.QueryHandlerFunc(query.HandleFindUserQuery),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
@ -153,23 +153,6 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Init commands and queries
|
||||
if err := initCommands(ctn); err != nil {
|
||||
logger.Fatal(
|
||||
ctx,
|
||||
"could not init commands",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
|
||||
if err := initQueries(ctn); err != nil {
|
||||
logger.Fatal(
|
||||
ctx,
|
||||
"could not init queries",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Define base middlewares
|
||||
|
@ -3,9 +3,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/database"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/migration"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
@ -18,11 +18,13 @@ const (
|
||||
)
|
||||
|
||||
func applyMigration(ctx context.Context, ctn *service.Container) error {
|
||||
migr, err := migration.From(ctn)
|
||||
orm, err := orm.From(ctn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migr := orm.Migration()
|
||||
|
||||
// Register available migrations
|
||||
migr.Register(
|
||||
m000initialSchema(),
|
||||
@ -74,29 +76,31 @@ func applyMigration(ctx context.Context, ctn *service.Container) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func m000initialSchema() migration.Migration {
|
||||
return database.NewMigration(
|
||||
// nolint: gochecknoglobals
|
||||
var initialModels = []interface{}{
|
||||
&model.User{},
|
||||
}
|
||||
|
||||
func m000initialSchema() orm.Migration {
|
||||
return orm.NewDBMigration(
|
||||
"00_initial_schema",
|
||||
func(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx, `
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT,
|
||||
email TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
connected_at TIMESTAMPTZ,
|
||||
CONSTRAINT unique_email unique(email)
|
||||
);
|
||||
`)
|
||||
func(ctx context.Context, tx *gorm.DB) error {
|
||||
for _, m := range initialModels {
|
||||
if err := tx.AutoMigrate(m).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
},
|
||||
func(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx, `
|
||||
DROP TABLE users;
|
||||
`)
|
||||
func(ctx context.Context, tx *gorm.DB) error {
|
||||
for _, m := range initialModels {
|
||||
if err := tx.DropTableIfExists(m).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
4
go.mod
4
go.mod
@ -7,11 +7,15 @@ require (
|
||||
github.com/99designs/gqlgen v0.11.3
|
||||
github.com/caarlos0/env/v6 v6.2.2
|
||||
github.com/cortesi/modd v0.0.0-20200630120222-8983974e5450 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/go-chi/chi v4.1.0+incompatible
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/gorilla/websocket v1.2.0
|
||||
github.com/jackc/pgx v3.6.2+incompatible
|
||||
github.com/jackc/pgx/v4 v4.7.1
|
||||
github.com/jinzhu/gorm v1.9.14
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/cors v1.7.0
|
||||
github.com/vektah/gqlparser/v2 v2.0.1
|
||||
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
|
30
go.sum
30
go.sum
@ -22,6 +22,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
|
||||
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
|
||||
@ -42,6 +43,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/bmatcuk/doublestar v1.3.0/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY=
|
||||
@ -50,6 +52,7 @@ github.com/caarlos0/env/v6 v6.2.2 h1:R0NIFXaB/LhwuGrjnsldzpnVNjFU/U+hTVHt+cq0yDY
|
||||
github.com/caarlos0/env/v6 v6.2.2/go.mod h1:3LpmfcAYCG6gCiSgDLaFR5Km1FRpPwFvBbRcjHar6Sw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
@ -73,12 +76,16 @@ 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/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
@ -90,9 +97,14 @@ github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@ -188,6 +200,12 @@ github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
|
||||
github.com/jackc/puddle v1.1.1 h1:PJAw7H/9hoWC4Kf3J8iNmL1SwA6E8vfsLqBiL+F6CtI=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/gorm v1.9.14 h1:Kg3ShyTPcM6nzVo148fRrcMO6MNKuqtOUwnzqMgVniM=
|
||||
github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||
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.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@ -203,7 +221,9 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
|
||||
@ -226,6 +246,8 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
@ -248,6 +270,8 @@ github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 h1:FLWDC+iIP9BWgY
|
||||
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
@ -258,6 +282,7 @@ 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/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
@ -300,6 +325,7 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@ -307,6 +333,7 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@ -331,6 +358,7 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -345,6 +373,8 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
3
internal/.gitignore
vendored
3
internal/.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/server.go
|
||||
/graph/generated
|
||||
/graph/generated
|
||||
/model/models_gen.go
|
@ -1,99 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/database"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
const (
|
||||
createConnectedUserStatement = `
|
||||
INSERT INTO users (email, connected_at) VALUES ($1, now())
|
||||
ON CONFLICT ON CONSTRAINT unique_email
|
||||
DO UPDATE SET connected_at = now();
|
||||
`
|
||||
createUserStatement = `
|
||||
INSERT INTO users (email) VALUES ($1)
|
||||
ON CONFLICT ON CONSTRAINT unique_email
|
||||
DO NOTHING;
|
||||
`
|
||||
)
|
||||
|
||||
type CreateUserCommandRequest struct {
|
||||
Email string
|
||||
Connected bool
|
||||
}
|
||||
|
||||
func HandleCreateUserCommand(ctx context.Context, cmd cqrs.Command) error {
|
||||
req, ok := cmd.Request().(*CreateUserCommandRequest)
|
||||
if !ok {
|
||||
return errors.WithStack(cqrs.ErrUnexpectedRequest)
|
||||
}
|
||||
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
pool, err := database.From(ctn)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
conn, err := pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer conn.Release()
|
||||
|
||||
if req.Connected {
|
||||
if err := createConnectedUser(ctx, conn, req.Email); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
if err := createUser(ctx, conn, req.Email); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createConnectedUser(ctx context.Context, conn *pgxpool.Conn, email string) error {
|
||||
_, err := conn.Conn().Prepare(
|
||||
ctx, "create_connected_user",
|
||||
createConnectedUserStatement,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if _, err := conn.Exec(ctx, "create_connected_user", email); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createUser(ctx context.Context, conn *pgxpool.Conn, email string) error {
|
||||
_, err := conn.Conn().Prepare(
|
||||
ctx, "create_user",
|
||||
createUserStatement,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if _, err := conn.Exec(ctx, "create_user", email); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -37,13 +37,19 @@ func NewFromFile(filepath string) (*Config, error) {
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
Address string `yaml:"address" env:"HTTP_ADDRESS"`
|
||||
CookieAuthenticationKey string `yaml:"cookieAuthenticationKey" env:"HTTP_COOKIE_AUTHENTICATION_KEY"`
|
||||
CookieEncryptionKey string `yaml:"cookieEncryptionKey" env:"HTTP_COOKIE_ENCRYPTION_KEY"`
|
||||
CookieMaxAge int `yaml:"cookieMaxAge" env:"HTTP_COOKIE_MAX_AGE"`
|
||||
TemplateDir string `yaml:"templateDir" env:"HTTP_TEMPLATE_DIR"`
|
||||
PublicDir string `yaml:"publicDir" env:"HTTP_PUBLIC_DIR"`
|
||||
FrontendURL string `yaml:"frontendURL" env:"HTTP_FRONTEND_URL"`
|
||||
Address string `yaml:"address" env:"HTTP_ADDRESS"`
|
||||
CookieAuthenticationKey string `yaml:"cookieAuthenticationKey" env:"HTTP_COOKIE_AUTHENTICATION_KEY"`
|
||||
CookieEncryptionKey string `yaml:"cookieEncryptionKey" env:"HTTP_COOKIE_ENCRYPTION_KEY"`
|
||||
CookieMaxAge int `yaml:"cookieMaxAge" env:"HTTP_COOKIE_MAX_AGE"`
|
||||
TemplateDir string `yaml:"templateDir" env:"HTTP_TEMPLATE_DIR"`
|
||||
PublicDir string `yaml:"publicDir" env:"HTTP_PUBLIC_DIR"`
|
||||
FrontendURL string `yaml:"frontendURL" env:"HTTP_FRONTEND_URL"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
}
|
||||
|
||||
type CORSConfig struct {
|
||||
AllowedOrigins []string `yaml:"allowedOrigins" env:"HTTP_CORS_ALLOWED_ORIGINS"`
|
||||
AllowCredentials bool `yaml:"allowCredentials" env:"HTTP_CORS_ALLOW_CREDENTIALS"`
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
@ -83,6 +89,10 @@ func NewDefault() *Config {
|
||||
TemplateDir: "template",
|
||||
PublicDir: "public",
|
||||
FrontendURL: "http://localhost:8080",
|
||||
CORS: CORSConfig{
|
||||
AllowedOrigins: []string{"http://localhost:8080"},
|
||||
AllowCredentials: true,
|
||||
},
|
||||
},
|
||||
OIDC: OIDCConfig{
|
||||
IssuerURL: "http://localhost:4444/",
|
||||
|
@ -1,79 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
type MigrationFunc func(ctx context.Context, tx pgx.Tx) error
|
||||
|
||||
type Migration struct {
|
||||
version string
|
||||
up MigrationFunc
|
||||
down MigrationFunc
|
||||
}
|
||||
|
||||
func (m *Migration) Version() string {
|
||||
return m.version
|
||||
}
|
||||
|
||||
func (m *Migration) Up(ctx context.Context) error {
|
||||
pool, err := m.getDatabaseService(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = WithTx(ctx, pool, func(ctx context.Context, tx pgx.Tx) error {
|
||||
return m.up(ctx, tx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not apply up migration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migration) Down(ctx context.Context) error {
|
||||
pool, err := m.getDatabaseService(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = WithTx(ctx, pool, func(ctx context.Context, tx pgx.Tx) error {
|
||||
return m.down(ctx, tx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not apply down migration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migration) getDatabaseService(ctx context.Context) (*pgxpool.Pool, error) {
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve service container")
|
||||
}
|
||||
|
||||
pool, err := From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve database service")
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func NewMigration(version string, up, down MigrationFunc) *Migration {
|
||||
return &Migration{
|
||||
version: version,
|
||||
up: up,
|
||||
down: down,
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
func ServiceProvider(dsn string) service.Provider {
|
||||
pool, err := pgxpool.Connect(context.Background(), dsn)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not connect to database")
|
||||
}
|
||||
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
const ServiceName service.Name = "database"
|
||||
|
||||
// From retrieves the database pool service in the given container.
|
||||
func From(container *service.Container) (*pgxpool.Pool, error) {
|
||||
service, err := container.Service(ServiceName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
srv, ok := service.(*pgxpool.Pool)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Must retrieves the database pool service in the given container or panic otherwise.
|
||||
func Must(container *service.Container) *pgxpool.Pool {
|
||||
srv, err := From(container)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(context.Context, pgx.Tx) error) error {
|
||||
tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not begin transaction")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
panic(errors.Wrap(err, "could not rollback transaction"))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(ctx, tx); err != nil {
|
||||
err := errors.Wrap(err, "could not apply down migration")
|
||||
|
||||
if rollbackErr := tx.Rollback(ctx); rollbackErr != nil {
|
||||
return errors.Wrap(err, rollbackErr.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return errors.Wrap(err, "could not commit transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type VersionResolver struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func (r *VersionResolver) Current(ctx context.Context) (string, error) {
|
||||
var version string
|
||||
|
||||
err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error {
|
||||
err := tx.QueryRow(ctx, `SELECT version FROM database_schema WHERE is_current = true;`).
|
||||
Scan(&version)
|
||||
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could execute version resolver init transaction")
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (r *VersionResolver) Set(ctx context.Context, version string) error {
|
||||
err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error {
|
||||
if version != "" {
|
||||
_, err := tx.Exec(ctx, `
|
||||
INSERT INTO database_schema (version, is_current, migrated_at)
|
||||
VALUES
|
||||
(
|
||||
$1,
|
||||
true,
|
||||
now()
|
||||
)
|
||||
ON CONFLICT ON CONSTRAINT unique_version
|
||||
DO UPDATE SET migrated_at = now(), is_current = true;
|
||||
`, version)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := tx.Exec(ctx, `
|
||||
UPDATE database_schema SET is_current = false, migrated_at = null WHERE version <> $1;
|
||||
`, version)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not update schema version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *VersionResolver) Init(ctx context.Context) error {
|
||||
err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS database_schema(
|
||||
version TEXT NOT NULL,
|
||||
migrated_at TIME,
|
||||
is_current BOOLEAN,
|
||||
CONSTRAINT unique_version UNIQUE(version)
|
||||
);`)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could execute version resolver init transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewVersionResolver(pool *pgxpool.Pool) *VersionResolver {
|
||||
return &VersionResolver{
|
||||
pool: pool,
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
|
||||
schema:
|
||||
- graph/*.graphqls
|
||||
- graph/*.graphql
|
||||
|
||||
# Where should the generated server code go?
|
||||
exec:
|
||||
@ -14,7 +14,7 @@ exec:
|
||||
|
||||
# Where should any generated models go?
|
||||
model:
|
||||
filename: graph/model/models_gen.go
|
||||
filename: model/models_gen.go
|
||||
package: model
|
||||
|
||||
# Where should the resolver implementations go?
|
||||
@ -35,7 +35,7 @@ resolver:
|
||||
# gqlgen will search for any type names in the schema in these go packages
|
||||
# if they match it will use them, otherwise it will generate them.
|
||||
autobind:
|
||||
- "forge.cadoles.com/Cadoles/daddy/internal/graph/model"
|
||||
- "forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
|
||||
# This section declares type mapping between the GraphQL and go type systems
|
||||
#
|
||||
|
25
internal/graph/helper.go
Normal file
25
internal/graph/helper.go
Normal file
@ -0,0 +1,25 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
func getDB(ctx context.Context) (*gorm.DB, error) {
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
orm, err := orm.From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return orm.DB(), nil
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Name *string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
ConnectedAt time.Time `json:"connectedAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
7
internal/graph/mutation.graphql
Normal file
7
internal/graph/mutation.graphql
Normal file
@ -0,0 +1,7 @@
|
||||
input ProfileChanges {
|
||||
name: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
updateProfile(changes: ProfileChanges!): User!
|
||||
}
|
20
internal/graph/mutation.resolvers.go
Normal file
20
internal/graph/mutation.resolvers.go
Normal file
@ -0,0 +1,20 @@
|
||||
package graph
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) UpdateProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) {
|
||||
return handleUpdateUserProfile(ctx, changes)
|
||||
}
|
||||
|
||||
// Mutation returns generated.MutationResolver implementation.
|
||||
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
@ -1,7 +1,3 @@
|
||||
# GraphQL schema example
|
||||
#
|
||||
# https://gqlgen.com/getting-started/
|
||||
|
||||
scalar Time
|
||||
|
||||
type User {
|
||||
@ -13,4 +9,4 @@ type User {
|
||||
|
||||
type Query {
|
||||
userProfile: User
|
||||
}
|
||||
}
|
@ -7,10 +7,10 @@ import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/model"
|
||||
model1 "forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
)
|
||||
|
||||
func (r *queryResolver) UserProfile(ctx context.Context) (*model.User, error) {
|
||||
func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) {
|
||||
return handleUserProfile(ctx)
|
||||
}
|
||||
|
@ -4,4 +4,6 @@ package graph
|
||||
//
|
||||
// It serves as dependency injection for your app, add any dependencies you require here.
|
||||
|
||||
//go:generate go run github.com/99designs/gqlgen
|
||||
|
||||
type Resolver struct{}
|
||||
|
@ -3,43 +3,56 @@ package graph
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/model"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/query"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
func handleUserProfile(ctx context.Context) (*model.User, error) {
|
||||
db, err := getDB(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
userEmail, err := session.UserEmail(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
ctn, err := container.From(ctx)
|
||||
repo := model.NewUserRepository(db)
|
||||
|
||||
user, err := repo.FindUserByEmail(ctx, userEmail)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
dispatcher, err := cqrs.From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
qry := &query.FindUserQueryRequest{
|
||||
Email: userEmail,
|
||||
}
|
||||
|
||||
result, err := dispatcher.Query(ctx, qry)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
findUserData, ok := result.Data().(*query.FindUserData)
|
||||
if !ok {
|
||||
return nil, errors.WithStack(cqrs.ErrUnexpectedData)
|
||||
}
|
||||
|
||||
return findUserData.User, nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) {
|
||||
db, err := getDB(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
userEmail, err := session.UserEmail(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo := model.NewUserRepository(db)
|
||||
|
||||
userChanges := &model.User{}
|
||||
|
||||
if changes.Name != nil {
|
||||
userChanges.Name = changes.Name
|
||||
}
|
||||
|
||||
user, err := repo.UpdateUserByEmail(ctx, userEmail, userChanges)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
package migration
|
||||
|
||||
import "context"
|
||||
|
||||
type Migration interface {
|
||||
Version() string
|
||||
Up(context.Context) error
|
||||
Down(context.Context) error
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
func ServiceProvider(resolver VersionResolver) service.Provider {
|
||||
manager := NewManager(resolver)
|
||||
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
return manager, nil
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
const ServiceName service.Name = "migration"
|
||||
|
||||
// From retrieves the migration service in the given container.
|
||||
func From(container *service.Container) (*Manager, error) {
|
||||
service, err := container.Service(ServiceName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
srv, ok := service.(*Manager)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Must retrieves the migration service in the given container or panic otherwise.
|
||||
func Must(container *service.Container) *Manager {
|
||||
srv, err := From(container)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package migration
|
||||
|
||||
import "context"
|
||||
|
||||
type VersionResolver interface {
|
||||
Current(context.Context) (string, error)
|
||||
Set(context.Context, string) error
|
||||
}
|
15
internal/model/user.go
Normal file
15
internal/model/user.go
Normal file
@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID *uint `gorm:"primary_key"`
|
||||
Name *string `json:"name"`
|
||||
Email string `json:"email" gorm:"unique;not null"`
|
||||
ConnectedAt time.Time `json:"connectedAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type ProfileChanges struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
74
internal/model/user_repository.go
Normal file
74
internal/model/user_repository.go
Normal file
@ -0,0 +1,74 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *UserRepository) CreateOrConnectUser(ctx context.Context, email string) (*User, error) {
|
||||
user := &User{
|
||||
Email: email,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := orm.WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
err := tx.Where("email = ?", email).FirstOrCreate(user).Error
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := tx.Model(user).UpdateColumn("connected_at", time.Now()).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create user")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
user := &User{
|
||||
Email: email,
|
||||
}
|
||||
|
||||
err := r.db.First(user, "email = ?", email).Error
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not find user")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateUserByEmail(ctx context.Context, email string, changes *User) (*User, error) {
|
||||
user := &User{
|
||||
Email: email,
|
||||
}
|
||||
|
||||
err := r.db.First(user, "email = ?", email).Error
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not find user")
|
||||
}
|
||||
|
||||
if err := r.db.Model(user).Updates(changes).Error; err != nil {
|
||||
return nil, errors.Wrap(err, "could not update user")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db}
|
||||
}
|
84
internal/orm/migration.go
Normal file
84
internal/orm/migration.go
Normal file
@ -0,0 +1,84 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
type MigrationFunc func(ctx context.Context, tx *gorm.DB) error
|
||||
|
||||
type Migration interface {
|
||||
Version() string
|
||||
Up(context.Context) error
|
||||
Down(context.Context) error
|
||||
}
|
||||
|
||||
type DBMigration struct {
|
||||
version string
|
||||
up MigrationFunc
|
||||
down MigrationFunc
|
||||
}
|
||||
|
||||
func (m *DBMigration) Version() string {
|
||||
return m.version
|
||||
}
|
||||
|
||||
func (m *DBMigration) Up(ctx context.Context) error {
|
||||
db, err := m.getDatabase(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = WithTx(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
return m.up(ctx, tx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not apply up migration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DBMigration) Down(ctx context.Context) error {
|
||||
db, err := m.getDatabase(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = WithTx(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
return m.down(ctx, tx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not apply down migration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DBMigration) getDatabase(ctx context.Context) (*gorm.DB, error) {
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve service container")
|
||||
}
|
||||
|
||||
orm, err := From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve orm service")
|
||||
}
|
||||
|
||||
return orm.DB(), nil
|
||||
}
|
||||
|
||||
func NewDBMigration(version string, up, down MigrationFunc) *DBMigration {
|
||||
return &DBMigration{
|
||||
version: version,
|
||||
up: up,
|
||||
down: down,
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package migration
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -11,12 +11,12 @@ var (
|
||||
ErrMigrationNotFound = errors.New("migration not found")
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
type MigrationManager struct {
|
||||
migrations []Migration
|
||||
resolver VersionResolver
|
||||
}
|
||||
|
||||
func (m *Manager) Up(ctx context.Context) error {
|
||||
func (m *MigrationManager) Up(ctx context.Context) error {
|
||||
currentVersion, err := m.resolver.Current(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve current version")
|
||||
@ -58,7 +58,7 @@ func (m *Manager) Up(ctx context.Context) error {
|
||||
return errors.WithStack(ErrMigrationNotFound)
|
||||
}
|
||||
|
||||
func (m *Manager) Down(ctx context.Context) error {
|
||||
func (m *MigrationManager) Down(ctx context.Context) error {
|
||||
currentVersion, err := m.resolver.Current(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve current version")
|
||||
@ -91,7 +91,7 @@ func (m *Manager) Down(ctx context.Context) error {
|
||||
return errors.WithStack(ErrMigrationNotFound)
|
||||
}
|
||||
|
||||
func (m *Manager) Latest(ctx context.Context) error {
|
||||
func (m *MigrationManager) Latest(ctx context.Context) error {
|
||||
for {
|
||||
isLatest, err := m.IsLatest(ctx)
|
||||
if err != nil {
|
||||
@ -108,15 +108,15 @@ func (m *Manager) Latest(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Register(migrations ...Migration) {
|
||||
func (m *MigrationManager) Register(migrations ...Migration) {
|
||||
m.migrations = migrations
|
||||
}
|
||||
|
||||
func (m *Manager) CurrentVersion(ctx context.Context) (string, error) {
|
||||
func (m *MigrationManager) CurrentVersion(ctx context.Context) (string, error) {
|
||||
return m.resolver.Current(ctx)
|
||||
}
|
||||
|
||||
func (m *Manager) LatestVersion() (string, error) {
|
||||
func (m *MigrationManager) LatestVersion() (string, error) {
|
||||
if len(m.migrations) == 0 {
|
||||
return "", errors.WithStack(ErrNoAvailableMigration)
|
||||
}
|
||||
@ -124,7 +124,7 @@ func (m *Manager) LatestVersion() (string, error) {
|
||||
return m.migrations[len(m.migrations)-1].Version(), nil
|
||||
}
|
||||
|
||||
func (m *Manager) IsLatest(ctx context.Context) (bool, error) {
|
||||
func (m *MigrationManager) IsLatest(ctx context.Context) (bool, error) {
|
||||
currentVersion, err := m.resolver.Current(ctx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "could not retrieve current version")
|
||||
@ -138,8 +138,8 @@ func (m *Manager) IsLatest(ctx context.Context) (bool, error) {
|
||||
return currentVersion == latestVersion, nil
|
||||
}
|
||||
|
||||
func NewManager(resolver VersionResolver) *Manager {
|
||||
return &Manager{
|
||||
func NewMigrationManager(resolver VersionResolver) *MigrationManager {
|
||||
return &MigrationManager{
|
||||
resolver: resolver,
|
||||
migrations: make([]Migration, 0),
|
||||
}
|
49
internal/orm/provider.go
Normal file
49
internal/orm/provider.go
Normal file
@ -0,0 +1,49 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
|
||||
// Import postgres dialect
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
)
|
||||
|
||||
func ServiceProvider(dialect, dsn string, debug bool) service.Provider {
|
||||
db, err := gorm.Open(dialect, dsn)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not connect to database")
|
||||
}
|
||||
|
||||
var srv *Service
|
||||
|
||||
if err == nil {
|
||||
db = db.LogMode(debug)
|
||||
|
||||
versionResolver := NewDBVersionResolver(db)
|
||||
ctx := context.Background()
|
||||
|
||||
err := versionResolver.Init(ctx)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not initialize version resolver")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
srv = &Service{
|
||||
db: db,
|
||||
migration: NewMigrationManager(versionResolver),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
}
|
47
internal/orm/service.go
Normal file
47
internal/orm/service.go
Normal file
@ -0,0 +1,47 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
const ServiceName service.Name = "orm"
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
migration *MigrationManager
|
||||
}
|
||||
|
||||
func (s *Service) DB() *gorm.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
func (s *Service) Migration() *MigrationManager {
|
||||
return s.migration
|
||||
}
|
||||
|
||||
// From retrieves the orm service in the given container.
|
||||
func From(container *service.Container) (*Service, error) {
|
||||
service, err := container.Service(ServiceName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
srv, ok := service.(*Service)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Must retrieves the orm pool service in the given container or panic otherwise.
|
||||
func Must(container *service.Container) *Service {
|
||||
srv, err := From(container)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
47
internal/orm/tx.go
Normal file
47
internal/orm/tx.go
Normal file
@ -0,0 +1,47 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func WithTx(ctx context.Context, db *gorm.DB, fn func(context.Context, *gorm.DB) error) error {
|
||||
tx := db.BeginTx(ctx, &sql.TxOptions{})
|
||||
|
||||
defer func() {
|
||||
if err := tx.Rollback().Error; err != nil && !isGormError(err, gorm.ErrInvalidTransaction) {
|
||||
panic(errors.Wrap(err, "could not rollback transaction"))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(ctx, tx); err != nil {
|
||||
err := errors.Wrap(err, "could not apply down migration")
|
||||
|
||||
if rollbackErr := tx.Rollback().Error; rollbackErr != nil {
|
||||
return errors.Wrap(err, rollbackErr.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return errors.Wrap(err, "could not commit transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isGormError(err error, compErr error) bool {
|
||||
if errs, ok := err.(gorm.Errors); ok {
|
||||
for _, err := range errs {
|
||||
if errors.Is(err, compErr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Is(err, compErr)
|
||||
}
|
112
internal/orm/version_resolver.go
Normal file
112
internal/orm/version_resolver.go
Normal file
@ -0,0 +1,112 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type VersionResolver interface {
|
||||
Current(context.Context) (string, error)
|
||||
Set(context.Context, string) error
|
||||
}
|
||||
|
||||
type DBVersionResolver struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type DatabaseVersion struct {
|
||||
ID uint `gorm:"primary_key"`
|
||||
Version string `gorm:"unique; not null"`
|
||||
MigratedAt time.Time
|
||||
IsCurrent bool
|
||||
}
|
||||
|
||||
func (r *DBVersionResolver) Current(ctx context.Context) (string, error) {
|
||||
var version string
|
||||
|
||||
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
dbVersion := &DatabaseVersion{}
|
||||
err := tx.Where("is_current = ?", true).First(dbVersion).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
version = dbVersion.Version
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could execute version resolver init transaction")
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (r *DBVersionResolver) Set(ctx context.Context, version string) error {
|
||||
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
dbVersion := &DatabaseVersion{
|
||||
Version: version,
|
||||
MigratedAt: time.Now(),
|
||||
}
|
||||
|
||||
if version != "" {
|
||||
if err := tx.FirstOrCreate(dbVersion).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := tx.Model(dbVersion).
|
||||
UpdateColumn("is_current", true).Error
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
err := tx.Model(&DatabaseVersion{}).
|
||||
Where("version <> ?", version).
|
||||
UpdateColumn("is_current", false).Error
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not update schema version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DBVersionResolver) Init(ctx context.Context) error {
|
||||
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&DatabaseVersion{}).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := tx.Model(&DatabaseVersion{}).AddUniqueIndex("idx_unique_version", "version").Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could execute version resolver init transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDBVersionResolver(db *gorm.DB) *DBVersionResolver {
|
||||
return &DBVersionResolver{
|
||||
db: db,
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/model"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/database"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
const (
|
||||
findUserStatement = `SELECT email, connected_at, created_at FROM users WHERE email = $1`
|
||||
)
|
||||
|
||||
type FindUserQueryRequest struct {
|
||||
Email string
|
||||
}
|
||||
|
||||
type FindUserData struct {
|
||||
User *model.User
|
||||
}
|
||||
|
||||
func HandleFindUserQuery(ctx context.Context, qry cqrs.Query) (interface{}, error) {
|
||||
req, ok := qry.Request().(*FindUserQueryRequest)
|
||||
if !ok {
|
||||
return nil, errors.WithStack(cqrs.ErrUnexpectedRequest)
|
||||
}
|
||||
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
pool, err := database.From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
conn, err := pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer conn.Release()
|
||||
|
||||
_, err = conn.Conn().Prepare(
|
||||
ctx, "find_user",
|
||||
findUserStatement,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user := &model.User{}
|
||||
|
||||
err = conn.QueryRow(ctx, "find_user", req.Email).
|
||||
Scan(&user.Email, &user.ConnectedAt, &user.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
data := &FindUserData{
|
||||
User: user,
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
@ -3,8 +3,8 @@ package route
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/command"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
||||
|
||||
@ -62,15 +62,11 @@ func handleLoginCallback(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatcher := cqrs.Must(ctn)
|
||||
db := orm.Must(ctn).DB()
|
||||
repo := model.NewUserRepository(db)
|
||||
|
||||
cmd := &command.CreateUserCommandRequest{
|
||||
Email: claims.Email,
|
||||
Connected: true,
|
||||
}
|
||||
|
||||
if _, err := dispatcher.Exec(ctx, cmd); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
if _, err := repo.CreateOrConnectUser(ctx, claims.Email); err != nil {
|
||||
panic(errors.Wrap(err, "could not upsert user"))
|
||||
}
|
||||
|
||||
if err := session.SaveUserEmail(w, r, claims.Email); err != nil {
|
||||
|
@ -3,6 +3,9 @@ package route
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||
oidc "forge.cadoles.com/wpetit/goweb-oidc"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
@ -21,6 +24,10 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
logger.F("postLogoutURL", conf.OIDC.PostLogoutRedirectURL),
|
||||
)
|
||||
|
||||
if err := session.ClearUserEmail(w, r, false); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
client.Logout(w, r, conf.OIDC.PostLogoutRedirectURL)
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,22 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
||||
oidc "forge.cadoles.com/wpetit/goweb-oidc"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/rs/cors"
|
||||
"gitlab.com/wpetit/goweb/static"
|
||||
)
|
||||
|
||||
@ -21,20 +28,39 @@ func Mount(r *chi.Mux, config *config.Config) error {
|
||||
r.Get("/logout/redirect", handleLogoutRedirect)
|
||||
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Use(cors.New(cors.Options{
|
||||
AllowedOrigins: config.HTTP.CORS.AllowedOrigins,
|
||||
AllowCredentials: config.HTTP.CORS.AllowCredentials,
|
||||
Debug: config.Debug,
|
||||
}).Handler)
|
||||
r.Use(oidc.Middleware)
|
||||
r.Use(session.UserEmailMiddleware)
|
||||
|
||||
gql := handler.NewDefaultServer(
|
||||
gql := handler.New(
|
||||
generated.NewExecutableSchema(generated.Config{
|
||||
Resolvers: &graph.Resolver{},
|
||||
}),
|
||||
)
|
||||
|
||||
gql.AddTransport(transport.POST{})
|
||||
gql.AddTransport(&transport.Websocket{
|
||||
KeepAlivePingInterval: 10 * time.Second,
|
||||
Upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// TODO Check WS connection origin
|
||||
return true
|
||||
},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
},
|
||||
})
|
||||
|
||||
if config.Debug {
|
||||
r.Get("/v1/graphql", playground.Handler("GraphQL playground", "/api/v1/graphql"))
|
||||
gql.Use(extension.Introspection{})
|
||||
r.Get("/v1/playground", playground.Handler("GraphQL playground", "/api/v1/graphql"))
|
||||
}
|
||||
|
||||
r.Post("/v1/graphql", gql.ServeHTTP)
|
||||
r.Handle("/v1/graphql", gql)
|
||||
})
|
||||
|
||||
notFoundHandler := r.NotFoundHandler()
|
||||
|
@ -61,6 +61,23 @@ func SaveUserEmail(w http.ResponseWriter, r *http.Request, email string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearUserEmail(w http.ResponseWriter, r *http.Request, saveSession bool) error {
|
||||
sess, err := getSession(w, r)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
sess.Unset(string(userEmailKey))
|
||||
|
||||
if saveSession {
|
||||
if err := sess.Save(w, r); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetUserEmail(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
sess, err := getSession(w, r)
|
||||
if err != nil {
|
Loading…
Reference in New Issue
Block a user