Compare commits
25 Commits
feature/au
...
pkg/dev/ub
Author | SHA1 | Date | |
---|---|---|---|
bd133fa9d9 | |||
8d9d839acf | |||
e990184a0b | |||
4a340529da | |||
bc9aa1721a | |||
c4373cce46 | |||
8708e30020 | |||
676ddf3bc8 | |||
7bf4c4f080 | |||
ab90365c9c | |||
303ea6b1d6 | |||
ccf911322b | |||
0cb6c7c67e | |||
ec6de8a217 | |||
17cd58d68f | |||
08bd11f4d9 | |||
99fb4ac6d9 | |||
2ceba1f219 | |||
7122677351 | |||
0d308acd5c | |||
36c253d4d7 | |||
ed219ddd11 | |||
758c166f27 | |||
05dd505d6b | |||
1d526a37d0 |
@ -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"
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
/vendor
|
||||
/data
|
||||
/bin
|
||||
/.env
|
||||
/.env
|
||||
/release
|
17
Makefile
17
Makefile
@ -1,12 +1,12 @@
|
||||
SHELL := /bin/bash
|
||||
|
||||
build: build-docker build-server
|
||||
build: build-server
|
||||
|
||||
build-docker:
|
||||
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
|
||||
@ -15,7 +15,10 @@ deps: generate
|
||||
cd client && npm install
|
||||
go get ./...
|
||||
|
||||
up: build-docker
|
||||
client-dist:
|
||||
cd client && NODE_ENV=production npm run build
|
||||
|
||||
up: docker
|
||||
docker-compose up
|
||||
|
||||
watch:
|
||||
@ -45,7 +48,11 @@ test:
|
||||
hydra-shell:
|
||||
docker-compose exec hydra /bin/sh
|
||||
|
||||
clean: down
|
||||
.PHONY: release
|
||||
release:
|
||||
./misc/script/release
|
||||
|
||||
clean:
|
||||
rm -rf client/node_modules bin data .env internal/graph/generated internal/graph/server.go
|
||||
rm -rf vendor
|
||||
go clean -modcache
|
@ -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)|
|
||||
|
179
client/package-lock.json
generated
179
client/package-lock.json
generated
@ -4,6 +4,43 @@
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.0.2.tgz",
|
||||
"integrity": "sha512-4ighan5Anlj4tK/tdUHs4Mi1njqXZ7AxRCVolz/H702DjPphAJfm+FRkIadPTmwz+OLO+d+tX+6V1VBshf02rg==",
|
||||
"requires": {
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"@wry/context": "^0.5.2",
|
||||
"@wry/equality": "^0.1.9",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"graphql-tag": "^2.10.4",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"optimism": "^0.12.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"symbol-observable": "^1.2.0",
|
||||
"ts-invariant": "^0.4.4",
|
||||
"tslib": "^1.10.0",
|
||||
"zen-observable": "^0.8.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wry/context": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.5.2.tgz",
|
||||
"integrity": "sha512-B/JLuRZ/vbEKHRUiGj6xiMojST1kHhu4WcreLfNN7q9DqQFrb97cWgf/kiYsPSUCAMVN0HzfFc8XjJdzgZzfjw==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"optimism": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.12.1.tgz",
|
||||
"integrity": "sha512-t8I7HM1dw0SECitBYAqFOVHoBAHEQBTeKjIL9y9ImHzAVkdyPK4ifTgM4VJRDtTUY4r/u5Eqxs4XcGPHaoPkeQ==",
|
||||
"requires": {
|
||||
"@wry/context": "^0.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
|
||||
@ -1230,7 +1267,8 @@
|
||||
"@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=="
|
||||
"integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
@ -1551,15 +1589,6 @@
|
||||
"@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",
|
||||
@ -1744,93 +1773,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@ -3235,14 +3177,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"bulma": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.5.tgz",
|
||||
"integrity": "sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw=="
|
||||
},
|
||||
"bulma-switch": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.0.tgz",
|
||||
"integrity": "sha512-myD38zeUfjmdduq+pXabhJEe3x2hQP48l/OI+Y0fO3HdDynZUY/VJygucvEAJKRjr4HxD5DnEm4yx+oDOBXpAA=="
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz",
|
||||
"integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ=="
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.0.0",
|
||||
@ -5615,11 +5552,6 @@
|
||||
"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",
|
||||
@ -6507,11 +6439,6 @@
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"jwt-decode": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
|
||||
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
@ -7466,14 +7393,6 @@
|
||||
"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",
|
||||
@ -8048,11 +7967,6 @@
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
||||
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
|
||||
},
|
||||
"querystring": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||
@ -11006,15 +10920,6 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "dadd-",
|
||||
"name": "daddy",
|
||||
"version": "0.0.0",
|
||||
"description": "Daddy",
|
||||
"main": "index.js",
|
||||
@ -51,20 +51,10 @@
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.0.2",
|
||||
"@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",
|
||||
"bulma": "^0.9.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",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^7.1.3",
|
||||
|
@ -1,20 +1,20 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
import { WorkgroupPage } from './WorkgroupPage/WorkgroupPage';
|
||||
|
||||
export class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/profile" exact component={ProfilePage} />
|
||||
<Route path="/workgroups/:id" exact component={WorkgroupPage} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
}
|
38
client/src/components/HomePage/Dashboard.tsx
Normal file
38
client/src/components/HomePage/Dashboard.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { WorkgroupsPanel } from './WorkgroupsPanel';
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<WorkgroupsPanel />
|
||||
</div>
|
||||
<div className="column">
|
||||
<div className="box">
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
<h3 className="is-size-3 subtitle level-item">D.A.Ds</h3>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<button disabled className="button is-primary level-item"><i className="fa fa-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<pre>TODO</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column">
|
||||
<div className="box">
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
<h3 className="is-size-3 subtitle level-item">Assemblées</h3>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<button disabled className="button is-primary level-item"><i className="fa fa-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<pre>TODO</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,26 +1,31 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { RootState } from '../../store/reducers/root';
|
||||
import { Dashboard } from './Dashboard';
|
||||
import { useUserProfileQuery } from '../../gql/queries/profile';
|
||||
import { WithLoader } from '../WithLoader';
|
||||
|
||||
export function HomePage() {
|
||||
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
|
||||
const { data, loading } = useUserProfileQuery();
|
||||
|
||||
const { userProfile } = (data || {});
|
||||
|
||||
return (
|
||||
<Page title="Daddy - Accueil">
|
||||
<Page title={userProfile ? 'Tableau de bord' : '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.email ?
|
||||
<p>Bonjour <span className="has-text-weight-bold">{currentUser.email}</span> !</p> :
|
||||
<p>Veuillez vous authentifier.</p>
|
||||
}
|
||||
<section className="mt-5">
|
||||
<WithLoader loading={loading}>
|
||||
{
|
||||
userProfile ?
|
||||
<Dashboard /> :
|
||||
<div className="columns">
|
||||
<div className="column is-4 is-offset-4">
|
||||
<div className="box">
|
||||
<p>Veuillez vous authentifier.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</WithLoader>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
|
98
client/src/components/HomePage/WorkgroupsPanel.tsx
Normal file
98
client/src/components/HomePage/WorkgroupsPanel.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Workgroup } from '../../types/workgroup';
|
||||
import { User } from '../../types/user';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useWorkgroupsQuery } from '../../gql/queries/workgroups';
|
||||
import { useUserProfileQuery } from '../../gql/queries/profile';
|
||||
import { WithLoader } from '../WithLoader';
|
||||
|
||||
export function WorkgroupsPanel() {
|
||||
const workgroupsQuery = useWorkgroupsQuery();
|
||||
const userProfileQuery = useUserProfileQuery();
|
||||
const [ state, setState ] = useState({ selectedTab: 0 });
|
||||
|
||||
const isLoading = userProfileQuery.loading || workgroupsQuery.loading;
|
||||
const { userProfile } = (userProfileQuery.data || {});
|
||||
const { workgroups } = (workgroupsQuery.data || {});
|
||||
|
||||
const filterTabs = [
|
||||
{
|
||||
label: "Mes groupes en cours",
|
||||
filter: workgroups => workgroups.filter((wg: Workgroup) => {
|
||||
return wg.closedAt === null && wg.members.some((u: User) => u.id === (userProfile ? userProfile.id : ''));
|
||||
})
|
||||
},
|
||||
{
|
||||
label: "Ouverts",
|
||||
filter: workgroups => workgroups.filter((wg: Workgroup) => !wg.closedAt)
|
||||
},
|
||||
{
|
||||
label: "Clos",
|
||||
filter: workgroups => workgroups.filter((wg: Workgroup) => !!wg.closedAt)
|
||||
}
|
||||
];
|
||||
|
||||
const selectTab = (tabIndex: number) => {
|
||||
setState(state => ({ ...state, selectedTab: tabIndex }));
|
||||
};
|
||||
|
||||
let workgroupsItems = [];
|
||||
|
||||
workgroupsItems = filterTabs[state.selectedTab].filter(workgroups || []).map((wg: Workgroup) => {
|
||||
return (
|
||||
<Link to={`/workgroups/${wg.id}`} key={`wg-${wg.id}`} className="panel-block">
|
||||
<span className="panel-icon">
|
||||
<i className="fas fa-users" aria-hidden="true"></i>
|
||||
</span>
|
||||
{wg.name}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<nav className="panel is-info">
|
||||
<div className="level panel-heading mb-0">
|
||||
<div className="level-left">
|
||||
<p className="level-item">
|
||||
Groupes de travail
|
||||
</p>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<Link to="/workgroups/new" className="button level-item is-outlined is-info is-inverted">
|
||||
<i className="icon fa fa-plus"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="panel-block">
|
||||
<p className="control has-icons-left">
|
||||
<input className="input" type="text" placeholder="Filtrer..." />
|
||||
<span className="icon is-left">
|
||||
<i className="fas fa-search" aria-hidden="true"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div> */}
|
||||
<WithLoader loading={isLoading}>
|
||||
<p className="panel-tabs">
|
||||
{
|
||||
filterTabs.map((tab, i) => {
|
||||
return (
|
||||
<a key={`workgroup-tab-${i}`}
|
||||
onClick={selectTab.bind(null, i)}
|
||||
className={i === state.selectedTab ? 'is-active' : ''}>
|
||||
{tab.label}
|
||||
</a>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
{
|
||||
workgroupsItems.length > 0 ?
|
||||
workgroupsItems :
|
||||
<a className="panel-block has-text-centered is-block">
|
||||
<em>Aucun groupe dans cet catégorie pour l'instant.</em>
|
||||
</a>
|
||||
}
|
||||
</WithLoader>
|
||||
</nav>
|
||||
)
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export class Loader extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="loader-container">
|
||||
<div className="lds-ripple">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,44 +1,64 @@
|
||||
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';
|
||||
import { useUserProfileQuery } from '../gql/queries/profile';
|
||||
import { WithLoader } from './WithLoader';
|
||||
|
||||
export function Navbar() {
|
||||
const isAuthenticated = useSelector<RootState>(state => state.auth.isAuthenticated);
|
||||
const userProfileQuery = useUserProfileQuery();
|
||||
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">
|
||||
{
|
||||
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}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-in-alt"></i>
|
||||
</span>
|
||||
<span>Se connecter</span>
|
||||
</a>
|
||||
}
|
||||
<WithLoader loading={userProfileQuery.loading}>
|
||||
<div className="buttons">
|
||||
{
|
||||
userProfileQuery.data && userProfileQuery.data.userProfile ?
|
||||
<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>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</WithLoader>
|
||||
</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';
|
||||
}
|
||||
}
|
40
client/src/components/ProfilePage/ProfilePage.tsx
Normal file
40
client/src/components/ProfilePage/ProfilePage.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { UserForm } from '../UserForm';
|
||||
import { User } from '../../types/user';
|
||||
import { useUserProfileQuery } from '../../gql/queries/profile';
|
||||
import { useUpdateUserProfileMutation } from '../../gql/mutations/profile';
|
||||
import { WithLoader } from '../WithLoader';
|
||||
|
||||
export function ProfilePage() {
|
||||
const userProfileQuery = useUserProfileQuery();
|
||||
const [ updateProfile, updateUserProfileMutation ] = useUpdateUserProfileMutation();
|
||||
const isLoading = updateUserProfileMutation.loading || userProfileQuery.loading;
|
||||
|
||||
const { userProfile } = (userProfileQuery.data || {});
|
||||
|
||||
const onUserChange = (user: User) => {
|
||||
if (userProfile.name !== user.name) {
|
||||
updateProfile({ variables: {changes: { 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>
|
||||
<WithLoader loading={isLoading || !userProfile}>
|
||||
{
|
||||
<UserForm onChange={onUserChange} user={userProfile} />
|
||||
}
|
||||
</WithLoader>
|
||||
</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: {
|
||||
id: user && user.id ? user.id : '',
|
||||
name: user && user.name ? user.name : '',
|
||||
email: user && user.email ? user.email : '',
|
||||
createdAt: user && user.createdAt ? user.createdAt : null,
|
||||
connectedAt: user && user.connectedAt ? user.connectedAt : null,
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
18
client/src/components/WithLoader.tsx
Normal file
18
client/src/components/WithLoader.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React, { Fragment, PropsWithChildren, FunctionComponent } from 'react';
|
||||
|
||||
export interface WithLoaderProps {
|
||||
loading?: boolean|boolean[]
|
||||
}
|
||||
|
||||
export const WithLoader: FunctionComponent<WithLoaderProps> = ({ loading, children }) => {
|
||||
const isLoading = Array.isArray(loading) ? loading.some(l => l) : loading;
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
isLoading ?
|
||||
<div>Chargement</div> :
|
||||
children
|
||||
}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
96
client/src/components/WorkgroupPage/InfoForm.tsx
Normal file
96
client/src/components/WorkgroupPage/InfoForm.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { Workgroup } from '../../types/workgroup';
|
||||
|
||||
export interface InfoFormProps {
|
||||
workgroup: Workgroup
|
||||
onChange?: (workgroup: Workgroup) => void
|
||||
}
|
||||
|
||||
export function InfoForm({ workgroup, onChange }: InfoFormProps) {
|
||||
const [ state, setState ] = useState({
|
||||
changed: false,
|
||||
workgroup: {
|
||||
id: workgroup && workgroup.id ? workgroup.id : '',
|
||||
name: workgroup && workgroup.name ? workgroup.name : '',
|
||||
createdAt: workgroup && workgroup.createdAt ? workgroup.createdAt : null,
|
||||
closedAt: workgroup && workgroup.closedAt ? workgroup.closedAt : null,
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setState({
|
||||
changed: false,
|
||||
workgroup: {
|
||||
id: workgroup && workgroup.id ? workgroup.id : '',
|
||||
name: workgroup && workgroup.name ? workgroup.name : '',
|
||||
createdAt: workgroup && workgroup.createdAt ? workgroup.createdAt : null,
|
||||
closedAt: workgroup && workgroup.closedAt ? workgroup.closedAt : null,
|
||||
}
|
||||
});
|
||||
}, [workgroup]);
|
||||
|
||||
const onSaveClick = () => {
|
||||
if (!state.changed) return;
|
||||
if (typeof onChange !== 'function') return;
|
||||
onChange(state.workgroup as Workgroup);
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
changed: false,
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const onWorkgroupAttrChange = function(attrName: string, evt: ChangeEvent<HTMLInputElement>) {
|
||||
const value = evt.currentTarget.value;
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
changed: true,
|
||||
workgroup: {
|
||||
...state.workgroup,
|
||||
[attrName]: value,
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form" style={{width: '100%'}}>
|
||||
<div className="field">
|
||||
<label className="label">Nom du groupe</label>
|
||||
<div className="control">
|
||||
<input type="text" className="input" value={state.workgroup.name}
|
||||
onChange={onWorkgroupAttrChange.bind(null, "name")} />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
state.workgroup.createdAt ?
|
||||
<div className="field">
|
||||
<label className="label">Date de création</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.workgroup.createdAt}</p>
|
||||
</div>
|
||||
</div>:
|
||||
null
|
||||
}
|
||||
{
|
||||
state.workgroup.closedAt ?
|
||||
<div className="field">
|
||||
<label className="label">Date de clôture</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.workgroup.closedAt}</p>
|
||||
</div>
|
||||
</div>:
|
||||
null
|
||||
}
|
||||
<div className="buttons is-right">
|
||||
<button disabled={!state.changed}
|
||||
className="button is-success" onClick={onSaveClick}>
|
||||
<span>Enregistrer</span>
|
||||
<span className="icon"><i className="fa fa-save"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
52
client/src/components/WorkgroupPage/InfoPanel.tsx
Normal file
52
client/src/components/WorkgroupPage/InfoPanel.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { User } from '../../types/user';
|
||||
import { Workgroup } from '../../types/workgroup';
|
||||
import { InfoForm } from './InfoForm';
|
||||
import { WithLoader } from '../WithLoader';
|
||||
import { useUpdateWorkgroupMutation, useCreateWorkgroupMutation } from '../../gql/mutations/workgroups';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
export interface InfoPanelProps {
|
||||
workgroup: Workgroup
|
||||
}
|
||||
|
||||
export const InfoPanel: FunctionComponent<InfoPanelProps> = ({ workgroup }) => {
|
||||
const [ updateWorkgroup, updateWorkgroupMutation ] = useUpdateWorkgroupMutation();
|
||||
const [ createWorkgroup, createWorkgroupMutation ] = useCreateWorkgroupMutation();
|
||||
const history = useHistory();
|
||||
const isLoading = updateWorkgroupMutation.loading || createWorkgroupMutation.loading;
|
||||
|
||||
const onWorkgroupChange = (formWorkgroup: Workgroup) => {
|
||||
const variables: any = { changes: {} };
|
||||
|
||||
if (workgroup.name !== formWorkgroup.name) {
|
||||
variables.changes.name = formWorkgroup.name;
|
||||
}
|
||||
|
||||
if (Object.keys(variables.changes).length === 0) return;
|
||||
|
||||
const isCreation = workgroup.id === '';
|
||||
if (isCreation) {
|
||||
createWorkgroup({variables})
|
||||
.then(({ data: { createWorkgroup } }) => {
|
||||
history.push(`/workgroups/${createWorkgroup.id}`);
|
||||
});
|
||||
} else {
|
||||
variables.workgroupId = workgroup.id;
|
||||
updateWorkgroup({variables});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="panel">
|
||||
<p className="panel-heading">
|
||||
Informations
|
||||
</p>
|
||||
<div className="panel-block">
|
||||
<WithLoader loading={isLoading}>
|
||||
<InfoForm workgroup={workgroup} onChange={onWorkgroupChange} />
|
||||
</WithLoader>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
35
client/src/components/WorkgroupPage/MembersPanel.tsx
Normal file
35
client/src/components/WorkgroupPage/MembersPanel.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { User } from '../../types/user';
|
||||
|
||||
export interface MembersPanelProps {
|
||||
users: User[]
|
||||
}
|
||||
|
||||
export const MembersPanel: FunctionComponent<MembersPanelProps> = ({ users }) => {
|
||||
return (
|
||||
<nav className="panel">
|
||||
<p className="panel-heading">
|
||||
Membres
|
||||
</p>
|
||||
{
|
||||
users.map(u => {
|
||||
return (
|
||||
<div key={`user-${u.id}`} className="panel-block">
|
||||
<span className="panel-icon">
|
||||
<i className="fas fa-user" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>{`${ u.name ? (u.name + ' - ') : '' }`}</span><span className="is-italic">{`${u.email}`}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
users.length === 0 ?
|
||||
<a className="panel-block has-text-centered is-block">
|
||||
<p className="is-italic">Aucun membre pour l'instant.</p>
|
||||
</a> :
|
||||
null
|
||||
}
|
||||
</nav>
|
||||
);
|
||||
}
|
138
client/src/components/WorkgroupPage/WorkgroupPage.tsx
Normal file
138
client/src/components/WorkgroupPage/WorkgroupPage.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import React, { useEffect, useState, Fragment } from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { WithLoader } from '../WithLoader';
|
||||
import { useParams } from 'react-router';
|
||||
import { useWorkgroupsQuery } from '../../gql/queries/workgroups';
|
||||
import { useUserProfileQuery } from '../../gql/queries/profile';
|
||||
import { MembersPanel } from './MembersPanel';
|
||||
import { User } from '../../types/user';
|
||||
import { InfoPanel } from './InfoPanel';
|
||||
import { Workgroup } from '../../types/workgroup';
|
||||
import { useJoinWorkgroupMutation, useLeaveWorkgroupMutation, useCloseWorkgroupMutation } from '../../gql/mutations/workgroups';
|
||||
|
||||
export function WorkgroupPage() {
|
||||
const { id } = useParams();
|
||||
const workgroupsQuery = useWorkgroupsQuery({
|
||||
variables:{
|
||||
filter: {
|
||||
ids: [id],
|
||||
}
|
||||
}
|
||||
});
|
||||
const userProfileQuery = useUserProfileQuery();
|
||||
const [ joinWorkgroup, joinWorkgroupMutation ] = useJoinWorkgroupMutation();
|
||||
const [ leaveWorkgroup, leaveWorkgroupMutation ] = useLeaveWorkgroupMutation();
|
||||
const [ closeWorkgroup, closeWorkgroupMutation ] = useCloseWorkgroupMutation();
|
||||
const [ state, setState ] = useState({
|
||||
userProfileId: '',
|
||||
workgroup: {
|
||||
id: '',
|
||||
name: '',
|
||||
closedAt: null,
|
||||
createdAt: null,
|
||||
members: [],
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!workgroupsQuery.data) return;
|
||||
setState(state => ({...state, workgroup:{ ...state.workgroup, ...workgroupsQuery.data.workgroups[0]}}));
|
||||
}, [workgroupsQuery.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userProfileQuery.data) return;
|
||||
setState(state => ({...state, userProfileId: userProfileQuery.data.userProfile.id }));
|
||||
}, [userProfileQuery.data]);
|
||||
|
||||
const onJoinWorkgroupClick = () => {
|
||||
joinWorkgroup({
|
||||
variables: {
|
||||
workgroupId: state.workgroup.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onLeaveWorkgroupClick = () => {
|
||||
leaveWorkgroup({
|
||||
variables: {
|
||||
workgroupId: state.workgroup.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onCloseWorkgroupClick = () => {
|
||||
closeWorkgroup({
|
||||
variables: {
|
||||
workgroupId: state.workgroup.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const isNew = state.workgroup.id === '';
|
||||
const isWorkgroupMember = state.workgroup.members.some(u => u.id === state.userProfileId);
|
||||
const isClosed = state.workgroup.closedAt !== null;
|
||||
|
||||
return (
|
||||
<Page title="Groupe de travail">
|
||||
<div className="container is-fluid">
|
||||
<section className="mt-5">
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
{
|
||||
isNew ?
|
||||
<div className="level-item">
|
||||
<div>
|
||||
<h2 className="is-size-3 title is-spaced">Nouveau</h2>
|
||||
<h3 className="is-size-5 subtitle">Groupe de travail</h3>
|
||||
</div>
|
||||
</div> :
|
||||
<div className="level-item">
|
||||
<div>
|
||||
<h2 className="is-size-3 title is-spaced">{state.workgroup.name}</h2>
|
||||
<h3 className="is-size-5 subtitle">Groupe de travail <span className="is-italic">{ isClosed ? '(clos)' : null }</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<div className="buttons is-right level-item">
|
||||
{
|
||||
isNew || isClosed ? null :
|
||||
<Fragment>
|
||||
{
|
||||
isWorkgroupMember ?
|
||||
<Fragment>
|
||||
<button onClick={onLeaveWorkgroupClick} className="button is-info is-warning is-medium">
|
||||
<span>Quitter</span>
|
||||
<span className="icon"><i className="fas fa-sign-out-alt"></i></span>
|
||||
</button>
|
||||
<button onClick={onCloseWorkgroupClick} className="button is-danger is-medium">
|
||||
<span>Clore</span>
|
||||
<span className="icon"><i className="far fa-times-circle"></i></span>
|
||||
</button>
|
||||
</Fragment> :
|
||||
<button onClick={onJoinWorkgroupClick} className="button is-info is-medium">
|
||||
<span>Rejoindre</span>
|
||||
<span className="icon"><i className="fas fa-user-plus"></i></span>
|
||||
</button>
|
||||
}
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WithLoader loading={[workgroupsQuery.loading, userProfileQuery.loading, joinWorkgroupMutation.loading, leaveWorkgroupMutation.loading]}>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<InfoPanel workgroup={state.workgroup as Workgroup} />
|
||||
</div>
|
||||
<div className="column">
|
||||
<MembersPanel users={state.workgroup.members as User[]} />
|
||||
</div>
|
||||
</div>
|
||||
</WithLoader>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
20
client/src/gql/client.tsx
Normal file
20
client/src/gql/client.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
|
||||
import { Config } from '../config';
|
||||
import { WebSocketLink } from "@apollo/client/link/ws";
|
||||
import { RetryLink } from "@apollo/client/link/retry";
|
||||
import { SubscriptionClient } from "subscriptions-transport-ws";
|
||||
|
||||
const subscriptionClient = new SubscriptionClient(Config.subscriptionEndpoint, {
|
||||
reconnect: true,
|
||||
});
|
||||
|
||||
const link = new RetryLink({attempts: {max: 2}}).split(
|
||||
(operation) => operation.operationName === 'subscription',
|
||||
new WebSocketLink(subscriptionClient),
|
||||
new HttpLink({ uri: Config.graphQLEndpoint, credentials: 'include' })
|
||||
);
|
||||
|
||||
export const client = new ApolloClient<any>({
|
||||
cache: new InMemoryCache(),
|
||||
link: link,
|
||||
});
|
15
client/src/gql/mutations/profile.tsx
Normal file
15
client/src/gql/mutations/profile.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { gql, useQuery, useMutation } from '@apollo/client';
|
||||
|
||||
const MUTATION_UPDATE_USER_PROFILE = gql`
|
||||
mutation updateUserProfile($changes: ProfileChanges!) {
|
||||
updateProfile(changes: $changes) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
connectedAt,
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useUpdateUserProfileMutation() {
|
||||
return useMutation(MUTATION_UPDATE_USER_PROFILE);
|
||||
}
|
96
client/src/gql/mutations/workgroups.tsx
Normal file
96
client/src/gql/mutations/workgroups.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { gql, useQuery, useMutation } from '@apollo/client';
|
||||
|
||||
const MUTATION_UPDATE_WORKGROUP = gql`
|
||||
mutation updateWorkgroup($workgroupId: ID!, $changes: WorkgroupChanges!) {
|
||||
updateWorkgroup(workgroupId: $workgroupId, changes: $changes) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
name,
|
||||
email
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useUpdateWorkgroupMutation() {
|
||||
return useMutation(MUTATION_UPDATE_WORKGROUP);
|
||||
}
|
||||
|
||||
const MUTATION_CREATE_WORKGROUP = gql`
|
||||
mutation createWorkgroup($changes: WorkgroupChanges!) {
|
||||
createWorkgroup(changes: $changes) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
name,
|
||||
email
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useCreateWorkgroupMutation() {
|
||||
return useMutation(MUTATION_CREATE_WORKGROUP);
|
||||
}
|
||||
|
||||
const MUTATION_JOIN_WORKGROUP = gql`
|
||||
mutation joinWorkgroup($workgroupId: ID!) {
|
||||
joinWorkgroup(workgroupId: $workgroupId) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
name,
|
||||
email
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useJoinWorkgroupMutation() {
|
||||
return useMutation(MUTATION_JOIN_WORKGROUP);
|
||||
}
|
||||
|
||||
const MUTATION_LEAVE_WORKGROUP = gql`
|
||||
mutation leaveWorkgroup($workgroupId: ID!) {
|
||||
leaveWorkgroup(workgroupId: $workgroupId) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
name,
|
||||
email
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useLeaveWorkgroupMutation() {
|
||||
return useMutation(MUTATION_LEAVE_WORKGROUP);
|
||||
}
|
||||
|
||||
const MUTATION_CLOSE_WORKGROUP = gql`
|
||||
mutation closeWorkgroup($workgroupId: ID!) {
|
||||
closeWorkgroup(workgroupId: $workgroupId) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
name,
|
||||
email
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useCloseWorkgroupMutation() {
|
||||
return useMutation(MUTATION_CLOSE_WORKGROUP);
|
||||
}
|
16
client/src/gql/queries/profile.tsx
Normal file
16
client/src/gql/queries/profile.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
|
||||
const QUERY_USER_PROFILE = gql`
|
||||
query userProfile {
|
||||
userProfile {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
createdAt,
|
||||
connectedAt
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useUserProfileQuery() {
|
||||
return useQuery(QUERY_USER_PROFILE);
|
||||
}
|
21
client/src/gql/queries/workgroups.tsx
Normal file
21
client/src/gql/queries/workgroups.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
|
||||
const QUERY_WORKGROUP = gql`
|
||||
query workgroups($filter: WorkgroupsFilter) {
|
||||
workgroups(filter: $filter) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
email,
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function useWorkgroupsQuery(options = {}) {
|
||||
return useQuery(QUERY_WORKGROUP, options);
|
||||
}
|
@ -2,15 +2,18 @@ import './sass/_all.scss';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './components/App';
|
||||
import { Config } from './config';
|
||||
import { client } from './gql/client';
|
||||
|
||||
import '@fortawesome/fontawesome-free/js/fontawesome'
|
||||
import '@fortawesome/fontawesome-free/js/solid'
|
||||
import '@fortawesome/fontawesome-free/js/regular'
|
||||
import '@fortawesome/fontawesome-free/js/brands'
|
||||
import './resources/favicon.png';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
<ApolloProvider client={client}>
|
||||
<App />
|
||||
</ApolloProvider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
|
@ -1,4 +1,3 @@
|
||||
@import 'bulma/bulma.sass';
|
||||
@import 'bulma-switch/dist/css/bulma-switch.sass';
|
||||
@import '_base.scss';
|
||||
@import '_loader.scss';
|
@ -1,11 +0,0 @@
|
||||
import { Action } from "redux";
|
||||
|
||||
export const SET_CURRENT_USER = 'SET_CURRENT_USER';
|
||||
|
||||
export interface setCurrentUserAction extends Action {
|
||||
email: string
|
||||
}
|
||||
|
||||
export function setCurrentUser(email: string): setCurrentUserAction {
|
||||
return { type: SET_CURRENT_USER, email };
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { Action } from "redux";
|
||||
import { User } from "../../types/user";
|
||||
|
||||
export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST';
|
||||
export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
|
||||
export const FETCH_PROFILE_FAILURE = 'FETCH_PROFILE_FAILURE';
|
||||
|
||||
export interface fetchProfileRequestAction extends Action {
|
||||
|
||||
}
|
||||
|
||||
export interface fetchProfileSuccessAction extends Action {
|
||||
profile: User
|
||||
}
|
||||
|
||||
|
||||
export function fetchProfile(): fetchProfileRequestAction {
|
||||
return { type: FETCH_PROFILE_REQUEST }
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
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";
|
||||
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean
|
||||
currentUser: User
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
isAuthenticated: false,
|
||||
currentUser: null,
|
||||
};
|
||||
|
||||
export function authReducer(state = defaultState, action: Action): AuthState {
|
||||
switch (action.type) {
|
||||
case SET_CURRENT_USER:
|
||||
return handleSetCurrentUser(state, action as setCurrentUserAction);
|
||||
case FETCH_PROFILE_SUCCESS:
|
||||
return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction);
|
||||
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction): AuthState {
|
||||
return {
|
||||
...state,
|
||||
isAuthenticated: true,
|
||||
currentUser: {
|
||||
email
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState {
|
||||
return {
|
||||
...state,
|
||||
isAuthenticated: true,
|
||||
currentUser: {
|
||||
email: profile.email,
|
||||
connectedAt: profile.connectedAt,
|
||||
createdAt: profile.createdAt,
|
||||
}
|
||||
};
|
||||
};
|
@ -1,32 +0,0 @@
|
||||
import { Action } from "redux";
|
||||
|
||||
export interface FlagsState {
|
||||
actions: { [actionName: string]: ActionState }
|
||||
}
|
||||
|
||||
export interface ActionState {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
actions: {}
|
||||
};
|
||||
|
||||
export function flagsReducer(state = defaultState, action: Action): FlagsState {
|
||||
const matches = (/^(.*)_((SUCCESS)|(FAILURE)|(REQUEST))$/).exec(action.type);
|
||||
|
||||
if(!matches) return state;
|
||||
|
||||
const actionPrefix = matches[1];
|
||||
|
||||
return {
|
||||
...state,
|
||||
actions: {
|
||||
...state.actions,
|
||||
[actionPrefix]: {
|
||||
isLoading: matches[2] === 'REQUEST'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { flagsReducer, FlagsState } from './flags';
|
||||
import { authReducer, AuthState } from './auth';
|
||||
|
||||
export interface RootState {
|
||||
auth: AuthState,
|
||||
flags: FlagsState,
|
||||
}
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
flags: flagsReducer,
|
||||
auth: authReducer,
|
||||
});
|
@ -1,21 +0,0 @@
|
||||
import { UnauthorizedError } from "../../util/daddy";
|
||||
import { all, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
export function* failureRootSaga() {
|
||||
yield all([
|
||||
takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* failuresSaga(action) {
|
||||
if (action.error instanceof UnauthorizedError) {
|
||||
// TODO Implements better authorization error handling
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
export function patternFromRegExp(re: any) {
|
||||
return (action: any) => {
|
||||
return re.test(action.type);
|
||||
};
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { all, put } from "redux-saga/effects";
|
||||
import { fetchProfile } from "../actions/profile";
|
||||
|
||||
export function* initRootSaga() {
|
||||
yield all([
|
||||
fetchUserProfileSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
export function* fetchUserProfileSaga() {
|
||||
yield put(fetchProfile());
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
import { failureRootSaga } from './failure';
|
||||
import { initRootSaga } from './init';
|
||||
import { usersRootSaga } from './users';
|
||||
|
||||
export function* rootSaga() {
|
||||
yield all([
|
||||
initRootSaga(),
|
||||
failureRootSaga(),
|
||||
usersRootSaga(),
|
||||
]);
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
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 } 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 = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint);
|
||||
|
||||
let profile: User;
|
||||
try {
|
||||
profile = yield client.fetchProfile().then(result => result.userProfile);
|
||||
console.log(profile);
|
||||
} catch(err) {
|
||||
yield put({ type: FETCH_PROFILE_FAILURE, err });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({type: FETCH_PROFILE_SUCCESS, profile });
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
export function selectFlagsIsLoading(state: any, ...actionPrefixes: any[]) {
|
||||
const { actions } = state.flags;
|
||||
return actionPrefixes.reduce((isLoading, prefix) => {
|
||||
if (!(prefix in actions)) return isLoading;
|
||||
return isLoading || actions[prefix].isLoading;
|
||||
}, false);
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
import { createStore, applyMiddleware, compose } from 'redux'
|
||||
import createSagaMiddleware from 'redux-saga'
|
||||
import { rootReducer } from './reducers/root'
|
||||
import { rootSaga } from './sagas/root'
|
||||
|
||||
let reduxMiddlewares = [];
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const createLogger = require('redux-logger').createLogger;
|
||||
const loggerMiddleware = createLogger({
|
||||
collapsed: true,
|
||||
diff: true
|
||||
});
|
||||
reduxMiddlewares.push(loggerMiddleware);
|
||||
}
|
||||
|
||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
// create the saga middleware
|
||||
const sagaMiddleware = createSagaMiddleware()
|
||||
reduxMiddlewares.push(sagaMiddleware);
|
||||
|
||||
// mount it on the Store
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
composeEnhancers(applyMiddleware(...reduxMiddlewares)),
|
||||
)
|
||||
|
||||
// then run the saga
|
||||
sagaMiddleware.run(rootSaga);
|
@ -1,5 +1,7 @@
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
connectedAt?: Date
|
||||
createdAt?: Date
|
||||
}
|
9
client/src/types/workgroup.ts
Normal file
9
client/src/types/workgroup.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { User } from "./user";
|
||||
|
||||
export interface Workgroup {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: Date
|
||||
closedAt: Date
|
||||
members: [User]
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import ApolloClient 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 } from 'apollo-utilities';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor(...args: any[]) {
|
||||
super(...args)
|
||||
Object.setPrototypeOf(this, UnauthorizedError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
let client: DaddyClient
|
||||
|
||||
export function getClient(graphQLEndpoint: string, subscriptionEndpoint: string): DaddyClient {
|
||||
if (!client) {
|
||||
client = new DaddyClient(graphQLEndpoint, subscriptionEndpoint);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export class DaddyClient {
|
||||
|
||||
gql: ApolloClient<InMemoryCache>
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
this.gql = new ApolloClient<any>({
|
||||
link: link,
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
}
|
||||
|
||||
fetchProfile() {
|
||||
return this.gql.query({
|
||||
query: gql`
|
||||
query {
|
||||
userProfile {
|
||||
email,
|
||||
createdAt,
|
||||
connectedAt
|
||||
}
|
||||
}`
|
||||
})
|
||||
.then(this.assertAuthorization)
|
||||
}
|
||||
|
||||
assertAuthorization({ status, data }: any) {
|
||||
if (status === 401) return Promise.reject(new UnauthorizedError());
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
@ -3,17 +3,16 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/migration"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"github.com/wader/gormstore"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/database"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||
oidc "forge.cadoles.com/wpetit/goweb-oidc"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
"gitlab.com/wpetit/goweb/service/build"
|
||||
@ -51,23 +50,36 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con
|
||||
conf.HTTP.CookieEncryptionKey = string(cookieEncryptionKey)
|
||||
}
|
||||
|
||||
ctn.Provide(orm.ServiceName, orm.ServiceProvider("postgres", conf.Database.DSN, conf.Debug))
|
||||
|
||||
orm, err := orm.From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Create and initialize HTTP session service provider
|
||||
cookieStore := sessions.NewCookieStore(
|
||||
sessionStore := gormstore.NewOptions(
|
||||
orm.DB(),
|
||||
gormstore.Options{
|
||||
TableName: "sessions",
|
||||
SkipCreateTable: false,
|
||||
},
|
||||
[]byte(conf.HTTP.CookieAuthenticationKey),
|
||||
[]byte(conf.HTTP.CookieEncryptionKey),
|
||||
)
|
||||
|
||||
quit := make(chan struct{})
|
||||
go sessionStore.PeriodicCleanup(1*time.Hour, quit)
|
||||
|
||||
// Define default cookie options
|
||||
cookieStore.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: conf.HTTP.CookieMaxAge,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
sessionStore.SessionOpts.Path = "/"
|
||||
sessionStore.SessionOpts.HttpOnly = true
|
||||
sessionStore.SessionOpts.MaxAge = conf.HTTP.CookieMaxAge
|
||||
sessionStore.SessionOpts.SameSite = http.SameSiteStrictMode
|
||||
|
||||
ctn.Provide(
|
||||
session.ServiceName,
|
||||
gorilla.ServiceProvider("daddy", cookieStore),
|
||||
gorilla.ServiceProvider("daddy", sessionStore),
|
||||
)
|
||||
|
||||
// Create and expose config service provider
|
||||
@ -84,21 +96,5 @@ 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())
|
||||
|
||||
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,32 @@ 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{},
|
||||
&model.Workgroup{},
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
1
debian/compat
vendored
Normal file
1
debian/compat
vendored
Normal file
@ -0,0 +1 @@
|
||||
9
|
14
debian/control
vendored
Normal file
14
debian/control
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
Source: daddy
|
||||
Section: unknown
|
||||
Priority: optional
|
||||
Maintainer: Cadoles <contact@cadoles.com>
|
||||
Build-Depends: debhelper (>= 8.0.0), wget, ca-certificates, tar
|
||||
Standards-Version: 3.9.4
|
||||
Homepage: http://forge.cadoles.com/Cadoles/daddy
|
||||
Vcs-Git: http://forge.cadoles.com/Cadoles/daddy.git
|
||||
Vcs-Browser: http://forge.cadoles.com/Cadoles/daddy
|
||||
|
||||
Package: daddy
|
||||
Architecture: amd64
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||
Description: Daddy app
|
1
debian/daddy.links
vendored
Normal file
1
debian/daddy.links
vendored
Normal file
@ -0,0 +1 @@
|
||||
/etc/daddy/client-config.js /usr/share/daddy/public/config.js
|
11
debian/daddy.service
vendored
Normal file
11
debian/daddy.service
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Daddy app
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/daddy -workdir /usr/share/daddy -config /etc/daddy/config.yml
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
56
debian/rules
vendored
Normal file
56
debian/rules
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
export DH_VERBOSE=1
|
||||
|
||||
GO_VERSION := 1.13.5
|
||||
OS := linux
|
||||
ARCH := amd64
|
||||
GOPATH=$(HOME)/go
|
||||
|
||||
ifeq (, $(shell which go 2>/dev/null))
|
||||
override_dh_auto_build: install-go
|
||||
|
||||
override_dh_auto_clean: install-go
|
||||
endif
|
||||
|
||||
ifeq (, $(shell which node 2>/dev/null))
|
||||
override_dh_auto_build: install-nodejs
|
||||
endif
|
||||
|
||||
%:
|
||||
dh $@ --with systemd
|
||||
|
||||
override_dh_auto_build: $(GOPATH)
|
||||
GOPATH=$(GOPATH) PATH="$(PATH):/usr/local/go/bin:$(GOPATH)/bin" make deps
|
||||
GOPATH=$(GOPATH) PATH="$(PATH):/usr/local/go/bin:$(GOPATH)/bin" ARCH_TARGETS=$(ARCH) make release
|
||||
|
||||
$(GOPATH):
|
||||
mkdir -p $(GOPATH)
|
||||
|
||||
install-go:
|
||||
wget -nc https://dl.google.com/go/go$(GO_VERSION).$(OS)-$(ARCH).tar.gz
|
||||
tar -C /usr/local -xzf go$(GO_VERSION).$(OS)-$(ARCH).tar.gz
|
||||
|
||||
install-nodejs:
|
||||
wget -O- https://deb.nodesource.com/setup_12.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
override_dh_auto_install:
|
||||
|
||||
mkdir -p debian/daddy/usr/share/daddy
|
||||
mkdir -p debian/daddy/etc/daddy
|
||||
mkdir -p debian/daddy/usr/bin
|
||||
|
||||
cp -r release/server-$(OS)-$(ARCH)/* debian/daddy/usr/share/daddy/
|
||||
|
||||
mv debian/daddy/usr/share/daddy/bin/server debian/daddy/usr/bin/daddy
|
||||
mv debian/daddy/usr/share/daddy/server.conf debian/daddy/etc/daddy/config.yml
|
||||
mv debian/daddy/usr/share/daddy/public/config.js debian/daddy/etc/daddy/client-config.js
|
||||
|
||||
install -d debian/daddy
|
||||
|
||||
override_dh_strip:
|
||||
|
||||
override_dh_auto_test:
|
1
debian/source/format
vendored
Normal file
1
debian/source/format
vendored
Normal file
@ -0,0 +1 @@
|
||||
3.0 (native)
|
3
go.mod
3
go.mod
@ -7,14 +7,17 @@ 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
|
||||
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23
|
||||
gitlab.com/wpetit/goweb v0.0.0-20200707070104-985ce3eba3c2
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
)
|
||||
|
36
go.sum
36
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,15 @@ 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.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
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=
|
||||
@ -121,6 +134,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
|
||||
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
@ -188,6 +203,13 @@ 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.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
|
||||
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 +225,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 +250,10 @@ 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/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
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=
|
||||
@ -260,6 +288,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=
|
||||
@ -284,6 +313,8 @@ github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWp
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
|
||||
github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
|
||||
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23 h1:gtfR002LWpH9vQ1/GLbWBOTcS92cBi5PAR021lArKF8=
|
||||
github.com/wader/gormstore v0.0.0-20200328121358-65a111a20c23/go.mod h1:2z7nYWeR0xUeFNCmlyH6Qt6qigF+Kl/k4LbQbj6Ksus=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce h1:B3inZUHFr/FpA3jb+ZeSSHk3FSpB0xkQ0TjePhRokxw=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20200418152305-76dea96a46ce/go.mod h1:Gfv7cBOw1T2XwXMsLm1d9kAjMAdNtLMjPv+yCzRO9qk=
|
||||
@ -302,6 +333,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=
|
||||
@ -309,6 +341,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=
|
||||
@ -333,6 +366,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=
|
||||
@ -347,6 +381,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
|
||||
}
|
@ -55,7 +55,7 @@ type CORSConfig struct {
|
||||
type OIDCConfig struct {
|
||||
ClientID string `yaml:"clientId" env:"OIDC_CLIENT_ID"`
|
||||
ClientSecret string `yaml:"clientSecret" env:"OIDC_CLIENT_SECRET"`
|
||||
IssuerURL string `ymal:"issuerUrl" env:"OIDC_ISSUER_URL"`
|
||||
IssuerURL string `yaml:"issuerUrl" env:"OIDC_ISSUER_URL"`
|
||||
RedirectURL string `yaml:"redirectUrl" env:"OIDC_REDIRECT_URL"`
|
||||
PostLogoutRedirectURL string `yaml:"postLogoutRedirectURL" env:"OIDC_POST_LOGOUT_REDIRECT_URL"`
|
||||
}
|
||||
|
@ -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
|
||||
#
|
||||
|
48
internal/graph/helper.go
Normal file
48
internal/graph/helper.go
Normal file
@ -0,0 +1,48 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/session"
|
||||
"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
|
||||
}
|
||||
|
||||
func getSessionUser(ctx context.Context) (*model.User, *gorm.DB, error) {
|
||||
db, err := getDB(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
userEmail, err := session.UserEmail(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo := model.NewUserRepository(db)
|
||||
|
||||
user, err := repo.FindUserByEmail(ctx, userEmail)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return user, 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"`
|
||||
}
|
16
internal/graph/mutation.graphql
Normal file
16
internal/graph/mutation.graphql
Normal file
@ -0,0 +1,16 @@
|
||||
input ProfileChanges {
|
||||
name: String
|
||||
}
|
||||
|
||||
input WorkgroupChanges {
|
||||
name: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
updateProfile(changes: ProfileChanges!): User!
|
||||
joinWorkgroup(workgroupId: ID!): Workgroup!
|
||||
leaveWorkgroup(workgroupId: ID!): Workgroup!
|
||||
createWorkgroup(changes: WorkgroupChanges!): Workgroup!
|
||||
closeWorkgroup(workgroupId: ID!): Workgroup!
|
||||
updateWorkgroup(workgroupId: ID!, changes: WorkgroupChanges!): Workgroup!
|
||||
}
|
40
internal/graph/mutation.resolvers.go
Normal file
40
internal/graph/mutation.resolvers.go
Normal file
@ -0,0 +1,40 @@
|
||||
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)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) JoinWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
||||
return handleJoinWorkgroup(ctx, workgroupID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) LeaveWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
||||
return handleLeaveWorkgroup(ctx, workgroupID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||
return handleCreateWorkgroup(ctx, changes)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
||||
return handleCloseWorkgroup(ctx, workgroupID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) UpdateWorkgroup(ctx context.Context, workgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||
return handleUpdateWorkgroup(ctx, workgroupID, changes)
|
||||
}
|
||||
|
||||
// Mutation returns generated.MutationResolver implementation.
|
||||
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
27
internal/graph/query.graphql
Normal file
27
internal/graph/query.graphql
Normal file
@ -0,0 +1,27 @@
|
||||
scalar Time
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
name: String
|
||||
email: String!
|
||||
connectedAt: Time!
|
||||
createdAt: Time!
|
||||
workgroups:[Workgroup]!
|
||||
}
|
||||
|
||||
type Workgroup {
|
||||
id: ID!
|
||||
name: String
|
||||
createdAt: Time!
|
||||
closedAt: Time
|
||||
members: [User]!
|
||||
}
|
||||
|
||||
input WorkgroupsFilter {
|
||||
ids: [ID]
|
||||
}
|
||||
|
||||
type Query {
|
||||
userProfile: User
|
||||
workgroups(filter: WorkgroupsFilter): [Workgroup]!
|
||||
}
|
41
internal/graph/query.resolvers.go
Normal file
41
internal/graph/query.resolvers.go
Normal file
@ -0,0 +1,41 @@
|
||||
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"
|
||||
"strconv"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/graph/generated"
|
||||
model1 "forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
)
|
||||
|
||||
func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) {
|
||||
return handleUserProfile(ctx)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Workgroups(ctx context.Context, filter *model1.WorkgroupsFilter) ([]*model1.Workgroup, error) {
|
||||
return handleWorkgroups(ctx, filter)
|
||||
}
|
||||
|
||||
func (r *userResolver) ID(ctx context.Context, obj *model1.User) (string, error) {
|
||||
return strconv.FormatUint(uint64(obj.ID), 10), nil
|
||||
}
|
||||
|
||||
func (r *workgroupResolver) ID(ctx context.Context, obj *model1.Workgroup) (string, error) {
|
||||
return strconv.FormatUint(uint64(obj.ID), 10), nil
|
||||
}
|
||||
|
||||
// Query returns generated.QueryResolver implementation.
|
||||
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||
|
||||
// User returns generated.UserResolver implementation.
|
||||
func (r *Resolver) User() generated.UserResolver { return &userResolver{r} }
|
||||
|
||||
// Workgroup returns generated.WorkgroupResolver implementation.
|
||||
func (r *Resolver) Workgroup() generated.WorkgroupResolver { return &workgroupResolver{r} }
|
||||
|
||||
type queryResolver struct{ *Resolver }
|
||||
type userResolver struct{ *Resolver }
|
||||
type workgroupResolver struct{ *Resolver }
|
@ -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{}
|
||||
|
@ -1,16 +0,0 @@
|
||||
# GraphQL schema example
|
||||
#
|
||||
# https://gqlgen.com/getting-started/
|
||||
|
||||
scalar Time
|
||||
|
||||
type User {
|
||||
name: String
|
||||
email: String!
|
||||
connectedAt: Time!
|
||||
createdAt: Time!
|
||||
}
|
||||
|
||||
type Query {
|
||||
userProfile: User
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
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/graph/model"
|
||||
)
|
||||
|
||||
func (r *queryResolver) UserProfile(ctx context.Context) (*model.User, error) {
|
||||
return handleUserProfile(ctx)
|
||||
}
|
||||
|
||||
// Query returns generated.QueryResolver implementation.
|
||||
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||
|
||||
type queryResolver struct{ *Resolver }
|
@ -1,45 +0,0 @@
|
||||
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"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/cqrs"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
func handleUserProfile(ctx context.Context) (*model.User, error) {
|
||||
userEmail, err := session.UserEmail(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
ctn, err := container.From(ctx)
|
||||
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
|
||||
}
|
39
internal/graph/user_profile_handler.go
Normal file
39
internal/graph/user_profile_handler.go
Normal file
@ -0,0 +1,39 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func handleUserProfile(ctx context.Context) (*model.User, error) {
|
||||
user, _, err := getSessionUser(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) {
|
||||
user, db, err := getSessionUser(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, user.Email, userChanges)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
142
internal/graph/workgroup_handler.go
Normal file
142
internal/graph/workgroup_handler.go
Normal file
@ -0,0 +1,142 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func handleWorkgroups(ctx context.Context, filter *model.WorkgroupsFilter) ([]*model.Workgroup, error) {
|
||||
db, err := getDB(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo := model.NewWorkgroupRepository(db)
|
||||
|
||||
criteria := make([]interface{}, 0)
|
||||
|
||||
if filter != nil {
|
||||
if len(filter.Ids) > 0 {
|
||||
criteria = append(criteria, "id in (?)", filter.Ids)
|
||||
}
|
||||
}
|
||||
|
||||
workgroups, err := repo.FindWorkgroups(ctx, criteria...)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroups, nil
|
||||
}
|
||||
|
||||
func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) {
|
||||
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user, db, err := getSessionUser(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo := model.NewWorkgroupRepository(db)
|
||||
|
||||
workgroup, err := repo.AddUserToWorkgroup(ctx, user.ID, workgroupID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func handleLeaveWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) {
|
||||
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user, db, err := getSessionUser(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo := model.NewWorkgroupRepository(db)
|
||||
|
||||
workgroup, err := repo.RemoveUserFromWorkgroup(ctx, user.ID, workgroupID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||
db, err := getDB(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo := model.NewWorkgroupRepository(db)
|
||||
|
||||
workgroup, err := repo.CreateWorkgroup(ctx, changes)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func handleCloseWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) {
|
||||
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
db, err := getDB(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo := model.NewWorkgroupRepository(db)
|
||||
|
||||
workgroup, err := repo.CloseWorkgroup(ctx, workgroupID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func handleUpdateWorkgroup(ctx context.Context, rawWorkgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||
workgroupID, err := parseWorkgroupID(rawWorkgroupID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
db, err := getDB(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo := model.NewWorkgroupRepository(db)
|
||||
|
||||
workgroup, err := repo.UpdateWorkgroup(ctx, workgroupID, changes)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func parseWorkgroupID(workgroupID string) (uint, error) {
|
||||
workgroupID64, err := strconv.ParseUint(workgroupID, 10, 32)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return uint(workgroupID64), 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
|
||||
}
|
19
internal/model/user.go
Normal file
19
internal/model/user.go
Normal file
@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Name *string `json:"name"`
|
||||
Email string `json:"email" gorm:"unique;not null"`
|
||||
ConnectedAt time.Time `json:"connectedAt"`
|
||||
Workgroups []*Workgroup `gorm:"many2many:users_workgroups;"`
|
||||
}
|
||||
|
||||
type ProfileChanges struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
73
internal/model/user_repository.go
Normal file
73
internal/model/user_repository.go
Normal file
@ -0,0 +1,73 @@
|
||||
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,
|
||||
}
|
||||
|
||||
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.Model(user).Preload("Workgroups").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}
|
||||
}
|
18
internal/model/workgroup.go
Normal file
18
internal/model/workgroup.go
Normal file
@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
type Workgroup struct {
|
||||
gorm.Model
|
||||
Name *string `json:"name"`
|
||||
ClosedAt time.Time `json:"closedAt"`
|
||||
Members []*User `gorm:"many2many:users_workgroups;"`
|
||||
}
|
||||
|
||||
type WorkgroupChanges struct {
|
||||
Name *string `json:"name"`
|
||||
}
|
140
internal/model/workgroup_repository.go
Normal file
140
internal/model/workgroup_repository.go
Normal file
@ -0,0 +1,140 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type WorkgroupRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *WorkgroupRepository) FindWorkgroups(ctx context.Context, criteria ...interface{}) ([]*Workgroup, error) {
|
||||
workgroups := make([]*Workgroup, 0)
|
||||
if err := r.db.Model(&Workgroup{}).Preload("Members").Find(&workgroups, criteria...).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroups, nil
|
||||
}
|
||||
|
||||
func (r *WorkgroupRepository) UpdateWorkgroup(ctx context.Context, workgroupID uint, changes WorkgroupChanges) (*Workgroup, error) {
|
||||
workgroup := &Workgroup{
|
||||
Name: changes.Name,
|
||||
}
|
||||
workgroup.ID = workgroupID
|
||||
|
||||
err := r.db.Model(workgroup).
|
||||
Update(workgroup).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
err = r.db.Model(workgroup).Preload("Members").First(workgroup, "id = ?", workgroupID).Error
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func (r *WorkgroupRepository) CreateWorkgroup(ctx context.Context, changes WorkgroupChanges) (*Workgroup, error) {
|
||||
workgroup := &Workgroup{
|
||||
Name: changes.Name,
|
||||
}
|
||||
|
||||
if err := r.db.Model(&Workgroup{}).Create(workgroup).Error; err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func (r *WorkgroupRepository) CloseWorkgroup(ctx context.Context, workgroupID uint) (*Workgroup, error) {
|
||||
workgroup := &Workgroup{}
|
||||
|
||||
err := r.db.Model(workgroup).
|
||||
Where("id = ?", workgroupID).
|
||||
UpdateColumn("closedAt", time.Now()).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
err = r.db.Model(workgroup).Preload("Members").First(workgroup, "id = ?", workgroupID).Error
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func (r *WorkgroupRepository) AddUserToWorkgroup(ctx context.Context, userID, workgroupID uint) (*Workgroup, error) {
|
||||
user := &User{}
|
||||
|
||||
err := r.db.Model(user).Preload("Workgroups").First(user, "id = ?", userID).Error
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not find user")
|
||||
}
|
||||
|
||||
workgroup := &Workgroup{}
|
||||
workgroup.ID = workgroupID
|
||||
|
||||
err = r.db.Model(user).
|
||||
Association("Workgroups").
|
||||
Append(workgroup).
|
||||
Error
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not add user to workgroup")
|
||||
}
|
||||
|
||||
err = r.db.Model(workgroup).
|
||||
Preload("Members").
|
||||
First(workgroup, "id = ?", workgroupID).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func (r *WorkgroupRepository) RemoveUserFromWorkgroup(ctx context.Context, userID, workgroupID uint) (*Workgroup, error) {
|
||||
user := &User{}
|
||||
|
||||
err := r.db.First(user, "id = ?", userID).Error
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not find user")
|
||||
}
|
||||
|
||||
workgroup := &Workgroup{}
|
||||
workgroup.ID = workgroupID
|
||||
|
||||
err = r.db.Model(user).
|
||||
Association("Workgroups").
|
||||
Delete(workgroup).
|
||||
Error
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not add user to workgroup")
|
||||
}
|
||||
|
||||
err = r.db.Model(workgroup).
|
||||
Preload("Members").
|
||||
First(workgroup, "id = ?", workgroupID).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func NewWorkgroupRepository(db *gorm.DB) *WorkgroupRepository {
|
||||
return &WorkgroupRepository{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 {
|
||||
|
@ -33,7 +33,6 @@ func Mount(r *chi.Mux, config *config.Config) error {
|
||||
AllowCredentials: config.HTTP.CORS.AllowCredentials,
|
||||
Debug: config.Debug,
|
||||
}).Handler)
|
||||
r.Use(oidc.Middleware)
|
||||
r.Use(session.UserEmailMiddleware)
|
||||
|
||||
gql := handler.New(
|
||||
|
@ -21,7 +21,9 @@ func UserEmailMiddleware(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
userEmail, err := GetUserEmail(w, r)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not find user email"))
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := WithUserEmail(r.Context(), userEmail)
|
||||
|
15
misc/debian/nginx/daddy.conf
Normal file
15
misc/debian/nginx/daddy.conf
Normal file
@ -0,0 +1,15 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name daddy.local;
|
||||
|
||||
root /usr/share/daddy/client/public;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri /index.html =404;
|
||||
}
|
||||
|
||||
location /api/v1/graphql {
|
||||
proxy_pass http://127.0.0.1:8080/api/v1/graphql;
|
||||
}
|
||||
}
|
1
misc/debian/systemd/daddy.env
Normal file
1
misc/debian/systemd/daddy.env
Normal file
@ -0,0 +1 @@
|
||||
GO_ENV=prod
|
119
misc/script/release
Executable file
119
misc/script/release
Executable file
@ -0,0 +1,119 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
OS_TARGETS=(linux)
|
||||
ARCH_TARGETS=${ARCH_TARGETS:-amd64 arm 386}
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
PROJECT_DIR="$DIR/../.."
|
||||
|
||||
function build {
|
||||
|
||||
local name=$1
|
||||
local srcdir=$2
|
||||
local os=$3
|
||||
local arch=$4
|
||||
|
||||
local dirname="$name-$os-$arch"
|
||||
local destdir="$PROJECT_DIR/release/$dirname"
|
||||
|
||||
rm -rf "$destdir"
|
||||
mkdir -p "$destdir"
|
||||
|
||||
echo "building $dirname..."
|
||||
|
||||
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build \
|
||||
-ldflags="-s -w -X main.GitCommit=$(current_commit_ref) -X main.ProjectVersion=$(current_version)" \
|
||||
-gcflags=-trimpath="${PWD}" \
|
||||
-asmflags=-trimpath="${PWD}" \
|
||||
-o "$destdir/bin/$name" \
|
||||
"$srcdir"
|
||||
|
||||
if [ ! -z "$(which upx)" ]; then
|
||||
upx --best "$destdir/bin/$name"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
function current_commit_ref {
|
||||
git rev-list -1 HEAD
|
||||
}
|
||||
|
||||
function current_version {
|
||||
local latest_tag=$(git describe --abbrev=0 2>/dev/null)
|
||||
echo ${latest_tag:-0.0.0}
|
||||
}
|
||||
|
||||
function copy {
|
||||
|
||||
local name=$1
|
||||
local os=$2
|
||||
local arch=$3
|
||||
local src=$4
|
||||
local dest=$5
|
||||
|
||||
local dirname="$name-$os-$arch"
|
||||
local destdir="$PROJECT_DIR/release/$dirname"
|
||||
|
||||
echo "copying '$src' to '$destdir/$dest'..."
|
||||
|
||||
mkdir -p "$(dirname $destdir/$dest)"
|
||||
|
||||
cp -rfL $src "$destdir/$dest"
|
||||
|
||||
}
|
||||
|
||||
function dump_default_conf {
|
||||
# Generate and copy configuration file
|
||||
local command=$1
|
||||
local os=$2
|
||||
local arch=$3
|
||||
local tmp_conf=$(mktemp)
|
||||
|
||||
go run "$PROJECT_DIR/cmd/$command" -dump-config > "$tmp_conf"
|
||||
copy "$command" $os $arch "$tmp_conf" "$command.conf"
|
||||
rm -f "$tmp_conf"
|
||||
}
|
||||
|
||||
function compress {
|
||||
|
||||
local name=$1
|
||||
local os=$2
|
||||
local arch=$3
|
||||
|
||||
local dirname="$name-$os-$arch"
|
||||
local destdir="$PROJECT_DIR/release/$dirname"
|
||||
|
||||
echo "compressing $dirname..."
|
||||
tar -czf "$destdir.tar.gz" -C "$destdir/../" "$dirname"
|
||||
}
|
||||
|
||||
function release_server {
|
||||
|
||||
local os=$1
|
||||
local arch=$2
|
||||
|
||||
build 'server' "$PROJECT_DIR/cmd/server" $os $arch
|
||||
|
||||
dump_default_conf 'server' $os $arch
|
||||
|
||||
copy 'server' $os $arch "$PROJECT_DIR/README.md" "README.md"
|
||||
copy 'server' $os $arch "$PROJECT_DIR/client/dist" "public"
|
||||
|
||||
compress 'server' $os $arch
|
||||
|
||||
}
|
||||
|
||||
function main {
|
||||
|
||||
make client-dist
|
||||
|
||||
for os in ${OS_TARGETS[@]}; do
|
||||
for arch in ${ARCH_TARGETS[@]}; do
|
||||
release_server $os $arch
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
main
|
@ -1,4 +1,4 @@
|
||||
internal/graph/schema.graphqls
|
||||
internal/graph/*.graphql
|
||||
internal/gqlgen.yml {
|
||||
prep: make generate
|
||||
}
|
||||
@ -8,7 +8,7 @@ internal/gqlgen.yml {
|
||||
data/config.yml
|
||||
.env
|
||||
modd.conf {
|
||||
prep: make build-server
|
||||
prep: make build
|
||||
prep: [ -e data/config.yml ] || ( mkdir -p data && bin/server -dump-config > data/config.yml )
|
||||
prep: [ -e .env ] || ( cp .env.dist .env )
|
||||
prep: make migrate-latest
|
||||
|
Reference in New Issue
Block a user