Compare commits

...

4 Commits

Author SHA1 Message Date
Arnaud Delcasse 0f9fe384a0 Integrate support 2022-11-01 11:54:59 +01:00
Arnaud Delcasse d028256893 Improve sorting 2022-11-01 11:32:13 +01:00
Arnaud Delcasse 0dd4a723be Add documents 2022-11-01 00:40:20 +01:00
Arnaud Delcasse f4c2d61dc3 Sessions in etcd KV store instead of cookies 2022-10-30 20:11:36 +01:00
64 changed files with 1789 additions and 292 deletions

33
go.mod
View File

@ -2,22 +2,17 @@ module git.coopgo.io/coopgo-apps/parcoursmob
go 1.18
replace git.coopgo.io/coopgo-platform/mobility-accounts => ../../coopgo-platform/mobility-accounts/
// replace git.coopgo.io/coopgo-platform/mobility-accounts => ../../coopgo-platform/mobility-accounts/
replace git.coopgo.io/coopgo-platform/groups-management => ../../coopgo-platform/groups-management/
// replace git.coopgo.io/coopgo-platform/groups-management => ../../coopgo-platform/groups-management/
replace git.coopgo.io/coopgo-platform/fleets => ../../coopgo-platform/fleets/
replace git.coopgo.io/coopgo-platform/agenda => ../../coopgo-platform/agenda/
// replace git.coopgo.io/coopgo-platform/agenda => ../../coopgo-platform/agenda/
replace git.coopgo.io/coopgo-platform/emailing => ../../coopgo-platform/emailing/
// replace git.coopgo.io/coopgo-platform/emailing => ../../coopgo-platform/emailing/
require (
git.coopgo.io/coopgo-platform/agenda v0.0.0-00010101000000-000000000000
git.coopgo.io/coopgo-platform/emailing v0.0.0-00010101000000-000000000000
git.coopgo.io/coopgo-platform/fleets v0.0.0-00010101000000-000000000000
git.coopgo.io/coopgo-platform/groups-management v0.0.0-00010101000000-000000000000
git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-00010101000000-000000000000
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/fogleman/gg v1.3.0
github.com/go-playground/validator/v10 v10.11.0
@ -34,9 +29,18 @@ require (
google.golang.org/protobuf v1.28.1
)
require (
git.coopgo.io/coopgo-platform/agenda v0.0.0-20221017030035-4a26fc791c5b
git.coopgo.io/coopgo-platform/emailing v0.0.0-20221017030337-c71888d90c15
git.coopgo.io/coopgo-platform/fleets v0.0.0-20220905052643-be9ee8372fdd
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20221017025751-671dc9a2c544
git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20220906130339-b9a32e41bffe
)
require (
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/go-mail/mail v2.3.1+incompatible // indirect
github.com/go-playground/locales v0.14.0 // indirect
@ -47,17 +51,26 @@ require (
github.com/golang/snappy v0.0.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mb0/wkt v0.0.0-20170420051526-a30afd545ee1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.43 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect

34
go.sum
View File

@ -36,6 +36,16 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.coopgo.io/coopgo-platform/agenda v0.0.0-20221017030035-4a26fc791c5b h1:7kLW1khfGguZ2aL+QpWFwZmAdEcY1MsUjLdiRufjr2s=
git.coopgo.io/coopgo-platform/agenda v0.0.0-20221017030035-4a26fc791c5b/go.mod h1:wqPvfYmzGF2cfXbs8XE1P2j5UYqZwp/La0llkl7dUkc=
git.coopgo.io/coopgo-platform/emailing v0.0.0-20221017030337-c71888d90c15 h1:+ZI4nGE6mqZ6pc7N/BizheEPRXn6Z84Sj7ikwfP2ZcU=
git.coopgo.io/coopgo-platform/emailing v0.0.0-20221017030337-c71888d90c15/go.mod h1:rmbqiHVkONcECOoPlsXlxZnD315Tiz2oRnn1M7646Kg=
git.coopgo.io/coopgo-platform/fleets v0.0.0-20220905052643-be9ee8372fdd h1:7k5QMwMm6JQ0S2bNqXEe7Ouh8N9N3yAvcWB2GRcIZLk=
git.coopgo.io/coopgo-platform/fleets v0.0.0-20220905052643-be9ee8372fdd/go.mod h1:s9OIFCNcjBAbBzRNHwoCTYV6kAntPG9CpT3GVweGdTY=
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20221017025751-671dc9a2c544 h1:rMLP77uIEequVXXZ0X9G1iK2k+xvW/+58ggwxxI6gqY=
git.coopgo.io/coopgo-platform/groups-management v0.0.0-20221017025751-671dc9a2c544/go.mod h1:lozSy6qlIIYhvKKXscZzz28HAtS0qBDUTv5nofLRmYA=
git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20220906130339-b9a32e41bffe h1:4OKwfKybR0VsIw2dSM9RtqGWveWPt+JjtiiMIBrg/w0=
git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20220906130339-b9a32e41bffe/go.mod h1:1typNYtO+PQT6KG77vs/PUv0fO60/nbeSGZL2tt1LLg=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@ -85,6 +95,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -214,6 +225,8 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@ -222,6 +235,12 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -248,14 +267,23 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mb0/wkt v0.0.0-20170420051526-a30afd545ee1 h1:VCgV+ng800r1/AChRHzHbWCtQI06cPxoZQUljQHTyXc=
github.com/mb0/wkt v0.0.0-20170420051526-a30afd545ee1/go.mod h1:IhobDa5AIyiMAsnH/qkytD0NbG0JMOJ2ihQqe1NdXyg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.43 h1:14Q4lwblqTdlAmba05oq5xL0VBLHi06zS4yLnIkz6hI=
github.com/minio/minio-go/v7 v7.0.43/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@ -302,12 +330,16 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE=
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
@ -542,6 +574,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

View File

@ -4,8 +4,8 @@ import (
"net/http"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/cache"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
"github.com/spf13/viper"
)
@ -13,10 +13,10 @@ type APIHandler struct {
idp *identification.IdentificationProvider
config *viper.Viper
services *services.ServicesHandler
cache *cache.CacheHandler
cache cache.CacheHandler
}
func NewAPIHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, svc *services.ServicesHandler, cache *cache.CacheHandler) (*APIHandler, error) {
func NewAPIHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, svc *services.ServicesHandler, cache cache.CacheHandler) (*APIHandler, error) {
return &APIHandler{
idp: idp,
config: cfg,

101
handlers/api/export.go Normal file
View File

@ -0,0 +1,101 @@
package api
import (
"encoding/csv"
"fmt"
"net/http"
"sort"
"strconv"
"github.com/gorilla/mux"
)
type FlatMaps []map[string]any
func (maps FlatMaps) GetHeaders() (res []string) {
keys := map[string]bool{}
for _, m := range maps {
for k, _ := range m {
if _, ok := keys[k]; !ok {
keys[k] = true
res = append(res, k)
}
}
}
sort.Strings(res)
return
}
func (maps FlatMaps) GetValues() (res [][]string) {
headers := maps.GetHeaders()
for _, m := range maps {
line := []string{}
for _, k := range headers {
if v, ok := m[k]; ok && v != nil {
line = append(line, fmt.Sprint(v))
} else {
line = append(line, "")
}
}
res = append(res, line)
}
return
}
func (h APIHandler) CacheExport(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
cacheid := vars["cacheid"]
d, err := h.cache.Get(cacheid)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusNotFound)
return
}
if data, ok := d.([]any); ok {
flatmaps := FlatMaps{}
//fmt.Println(data)
for _, v := range data {
fmt.Println(v)
fm := map[string]any{}
flatten("", v.(map[string]any), fm)
fmt.Println(fm)
flatmaps = append(flatmaps, fm)
}
fmt.Println(flatmaps)
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=export-%s.csv", cacheid))
c := csv.NewWriter(w)
c.Write(flatmaps.GetHeaders())
c.WriteAll(flatmaps.GetValues())
return
}
w.WriteHeader(http.StatusNotFound)
}
func flatten(prefix string, src map[string]any, dest map[string]any) {
if len(prefix) > 0 {
prefix += "."
}
for k, v := range src {
switch child := v.(type) {
case map[string]any:
flatten(prefix+k, child, dest)
case []any:
for i := 0; i < len(child); i++ {
dest[prefix+k+"."+strconv.Itoa(i)] = child[i]
}
default:
fmt.Println(prefix+k, " : ", v)
dest[prefix+k] = v
}
}
}

View File

@ -18,6 +18,7 @@ func (h APIHandler) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
// Extract the ID Token from OAuth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
fmt.Println("issue retrieving token")
w.WriteHeader(http.StatusInternalServerError)
return
}
@ -40,7 +41,11 @@ func (h APIHandler) OAuth2Callback(w http.ResponseWriter, r *http.Request) {
delete(session.Values, "redirect")
}
session.Save(r, w)
if err = session.Save(r, w); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
http.Redirect(w, r, redirect, http.StatusFound)
}

View File

@ -8,9 +8,9 @@ import (
"io"
"net/http"
"sort"
"strings"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
@ -19,14 +19,6 @@ import (
"google.golang.org/protobuf/types/known/structpb"
)
type GroupsByName []groupstorage.Group
func (a GroupsByName) Len() int { return len(a) }
func (a GroupsByName) Less(i, j int) bool {
return strings.Compare(a[i].Data["name"].(string), a[j].Data["name"].(string)) < 0
}
func (a GroupsByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (h *ApplicationHandler) Administration(w http.ResponseWriter, r *http.Request) {
request := &groupsmanagement.GetGroupsRequest{
@ -47,7 +39,7 @@ func (h *ApplicationHandler) Administration(w http.ResponseWriter, r *http.Reque
groups = append(groups, g)
}
sort.Sort(GroupsByName(groups))
sort.Sort(sorting.GroupsByName(groups))
h.Renderer.Administration(w, r, groups)
}
@ -229,7 +221,7 @@ func (h *ApplicationHandler) AdministrationGroupInviteAdmin(w http.ResponseWrite
}
key := base64.RawURLEncoding.EncodeToString(b)
h.cache.PutWithTTL("onboarding/"+key, onboarding, 72*time.Hour)
h.cache.PutWithTTL("onboarding/"+key, onboarding, 168*time.Hour) // 1 week TTL
data := map[string]any{
"group": groupresp.Group.ToStorageType().Data["name"],
@ -247,6 +239,89 @@ func (h *ApplicationHandler) AdministrationGroupInviteAdmin(w http.ResponseWrite
return
}
func (h *ApplicationHandler) AdministrationGroupInviteMember(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
groupid := vars["groupid"]
groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), &groupsmanagement.GetGroupRequest{
Id: groupid,
Namespace: "parcoursmob_organizations",
})
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
group := groupresp.Group.ToStorageType()
r.ParseForm()
accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &accounts.GetAccountUsernameRequest{
Username: r.FormValue("username"),
Namespace: "parcoursmob",
})
if err == nil {
account := accountresp.Account.ToStorageType()
account.Data["groups"] = append(account.Data["groups"].([]any), group.ID)
as, _ := accounts.AccountFromStorageType(&account)
_, err = h.services.GRPC.MobilityAccounts.UpdateData(
context.TODO(),
&accounts.UpdateDataRequest{
Account: as,
},
)
fmt.Println(err)
data := map[string]any{
"group": group.Data["name"],
}
if err := h.emailing.Send("onboarding.existing_member", r.FormValue("username"), data); err != nil {
fmt.Println(err)
}
http.Redirect(w, r, "/app/group/settings", http.StatusFound)
return
} else {
// Onboard now administrator
onboarding := map[string]any{
"username": r.FormValue("username"),
"group": group.ID,
"admin": false,
}
b := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
key := base64.RawURLEncoding.EncodeToString(b)
h.cache.PutWithTTL("onboarding/"+key, onboarding, 168*time.Hour) // 1 week TTL
data := map[string]any{
"group": group.Data["name"],
"key": key,
}
if err := h.emailing.Send("onboarding.new_member", r.FormValue("username"), data); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
http.Redirect(w, r, "/app/administration/groups/"+group.ID, http.StatusFound)
return
}
func (h *ApplicationHandler) members() ([]*accounts.Account, error) {
resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(context.TODO(), &accounts.GetAccountsRequest{
Namespaces: []string{"parcoursmob"},

View File

@ -11,6 +11,7 @@ import (
formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/utils/form-validators"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
@ -21,12 +22,6 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
type EventsByStartdate []agendastorage.Event
func (e EventsByStartdate) Len() int { return len(e) }
func (e EventsByStartdate) Less(i, j int) bool { return e[i].Startdate.Before(e[j].Startdate) }
func (e EventsByStartdate) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
type EventsForm struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
@ -60,7 +55,7 @@ func (h *ApplicationHandler) AgendaHome(w http.ResponseWriter, r *http.Request)
responses = append(responses, e.ToStorageType())
}
sort.Sort(EventsByStartdate(responses))
sort.Sort(sorting.EventsByStartdate(responses))
groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
Groupids: groupids,

View File

@ -5,28 +5,30 @@ import (
"git.coopgo.io/coopgo-apps/parcoursmob/renderer"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/cache"
cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
"git.coopgo.io/coopgo-platform/emailing"
"github.com/spf13/viper"
)
type ApplicationHandler struct {
config *viper.Viper
Renderer *renderer.Renderer
services *services.ServicesHandler
cache *cache.CacheHandler
emailing *emailing.Mailer
config *viper.Viper
Renderer *renderer.Renderer
services *services.ServicesHandler
cache cache.CacheHandler
filestorage cache.FileStorage
emailing *emailing.Mailer
}
func NewApplicationHandler(cfg *viper.Viper, svc *services.ServicesHandler, cache *cache.CacheHandler, emailing *emailing.Mailer) (*ApplicationHandler, error) {
func NewApplicationHandler(cfg *viper.Viper, svc *services.ServicesHandler, cache cache.CacheHandler, filestorage cache.FileStorage, emailing *emailing.Mailer) (*ApplicationHandler, error) {
templates_root := cfg.GetString("templates.root")
renderer := renderer.NewRenderer(cfg, templates_root)
return &ApplicationHandler{
config: cfg,
Renderer: renderer,
services: svc,
cache: cache,
emailing: emailing,
config: cfg,
Renderer: renderer,
services: svc,
cache: cache,
filestorage: filestorage,
emailing: emailing,
}, nil
}

View File

@ -7,8 +7,10 @@ import (
"errors"
"fmt"
"image/png"
"io"
"log"
"net/http"
"sort"
"strconv"
"strings"
"time"
@ -16,10 +18,13 @@ import (
formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/utils/form-validators"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
profilepictures "git.coopgo.io/coopgo-apps/parcoursmob/utils/profile-pictures"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
filestorage "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
"git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
"github.com/google/uuid"
"github.com/gorilla/mux"
"google.golang.org/protobuf/types/known/structpb"
@ -29,8 +34,9 @@ type BeneficiariesForm struct {
FirstName string `json:"first_name" validate:"required"`
LastName string `json:"last_name" validate:"required"`
Email string `json:"email" validate:"required,email"`
Birthdate *time.Time `json:"birthdate"`
Birthdate *time.Time `json:"birthdate" validate:"required"`
PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"`
FileNumber string `json:"file_number" validate:"required"`
Address any `json:"address,omitempty"`
Gender string `json:"gender"`
}
@ -44,6 +50,8 @@ func (h *ApplicationHandler) BeneficiariesList(w http.ResponseWriter, r *http.Re
return
}
sort.Sort(sorting.BeneficiariesByName(accounts))
cacheid := uuid.NewString()
h.cache.PutWithTTL(cacheid, accounts, 1*time.Hour)
@ -115,6 +123,8 @@ func (h *ApplicationHandler) BeneficiaryDisplay(w http.ResponseWriter, r *http.R
vars := mux.Vars(r)
beneficiaryID := vars["beneficiaryid"]
documents := h.filestorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + beneficiaryID)
request := &mobilityaccounts.GetAccountRequest{
Id: beneficiaryID,
}
@ -145,7 +155,10 @@ func (h *ApplicationHandler) BeneficiaryDisplay(w http.ResponseWriter, r *http.R
bookings = append(bookings, b.ToStorageType())
}
h.Renderer.BeneficiaryDisplay(w, r, resp.Account.ToStorageType(), bookings)
beneficiaries_file_types := h.config.GetStringSlice("modules.beneficiaries.documents_types")
file_types_map := h.config.GetStringMapString("storage.files.file_types")
h.Renderer.BeneficiaryDisplay(w, r, resp.Account.ToStorageType(), bookings, beneficiaries_file_types, file_types_map, documents)
}
func (h *ApplicationHandler) BeneficiaryUpdate(w http.ResponseWriter, r *http.Request) {
@ -239,6 +252,63 @@ func (h *ApplicationHandler) BeneficiaryPicture(w http.ResponseWriter, r *http.R
}
}
func (h *ApplicationHandler) BeneficiaryDocuments(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
beneficiaryID := vars["beneficiaryid"]
//r.ParseForm()
r.ParseMultipartForm(10 * 1024 * 1024)
document_type := r.FormValue("type")
document_name := r.FormValue("name")
file, header, err := r.FormFile("file-upload")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
fileid := uuid.NewString()
metadata := map[string]string{
"type": document_type,
"name": document_name,
}
if err := h.filestorage.Put(file, filestorage.PREFIX_BENEFICIARIES, fmt.Sprintf("%s/%s_%s", beneficiaryID, fileid, header.Filename), header.Size, metadata); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", beneficiaryID), http.StatusFound)
}
func (h *ApplicationHandler) BeneficiaryDocumentDownload(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
beneficiaryID := vars["beneficiaryid"]
document := vars["document"]
file, info, err := h.filestorage.Get(filestorage.PREFIX_BENEFICIARIES, fmt.Sprintf("%s/%s", beneficiaryID, document))
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", info.ContentType)
if _, err = io.Copy(w, file); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
http.Redirect(w, r, fmt.Sprintf("/app/beneficiaries/%s", beneficiaryID), http.StatusFound)
}
func filterAccount(r *http.Request, a *mobilityaccounts.Account) bool {
searchFilter, ok := r.URL.Query()["search"]
@ -252,8 +322,8 @@ func filterAccount(r *http.Request, a *mobilityaccounts.Account) bool {
return true
}
func (h *ApplicationHandler) beneficiaries(r *http.Request) ([]any, error) {
var accounts = []any{}
func (h *ApplicationHandler) beneficiaries(r *http.Request) ([]mobilityaccountsstorage.Account, error) {
var accounts = []mobilityaccountsstorage.Account{}
g := r.Context().Value(identification.GroupKey)
if g == nil {
return accounts, errors.New("no group provided")
@ -301,6 +371,7 @@ func parseBeneficiariesForm(r *http.Request) (map[string]any, error) {
Email: r.PostFormValue("email"),
Birthdate: date,
PhoneNumber: r.PostFormValue("phone_number"),
FileNumber: r.PostFormValue("file_number"),
Gender: r.PostFormValue("gender"),
}

View File

@ -7,6 +7,7 @@ import (
"sort"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
"git.coopgo.io/coopgo-platform/groups-management/storage"
@ -69,7 +70,7 @@ func (h *ApplicationHandler) Dashboard(w http.ResponseWriter, r *http.Request) {
events = append(events, e.ToStorageType())
}
sort.Sort(EventsByStartdate(events))
sort.Sort(sorting.EventsByStartdate(events))
h.Renderer.Dashboard(w, r, accounts, count, count_members, events)

View File

@ -81,8 +81,8 @@ func (h *ApplicationHandler) JourneysSearch(w http.ResponseWriter, r *http.Reque
journeys, err = session.Journeys(context.Background(), request)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
// w.WriteHeader(http.StatusBadRequest)
// return
}
//CARPOOL
@ -93,8 +93,6 @@ func (h *ApplicationHandler) JourneysSearch(w http.ResponseWriter, r *http.Reque
// departuredatetime.Format("2006-01-02"), departuredatetime.Add(24*time.Hour).Format("2006-01-02"))
carpoolrequest := "https://api.rdex.ridygo.fr/journeys.json"
fmt.Println(carpoolrequest)
client := &http.Client{}
req, err := http.NewRequest("GET", carpoolrequest, nil)
if err != nil {
@ -113,11 +111,21 @@ func (h *ApplicationHandler) JourneysSearch(w http.ResponseWriter, r *http.Reque
fmt.Println(err)
}
err = json.NewDecoder(resp.Body).Decode(&carpoolresults)
if err != nil {
fmt.Println(err)
if err == nil && resp.StatusCode == http.StatusOK {
err = json.NewDecoder(resp.Body).Decode(&carpoolresults)
if err != nil {
fmt.Println(err)
}
if carpoolresults == nil {
carpoolresults = []any{}
}
} else {
carpoolresults = []any{}
}
fmt.Println(carpoolresults)
// Vehicles
vehiclerequest := &fleets.GetVehiclesRequest{

View File

@ -29,17 +29,12 @@ func (h *ApplicationHandler) SupportSend(w http.ResponseWriter, r *http.Request)
"user": current_user_claims["email"],
}
if err := h.emailing.Send("onboarding.Support_email", "support@parcoursmob.fr", data); err != nil {
if err := h.emailing.Send("support.request", "support@parcoursmob.fr", data); err != nil {
fmt.Println(err)
fmt.Println("error")
w.WriteHeader(http.StatusInternalServerError)
}
fmt.Println("success")
http.Redirect(w, r, "/app/", http.StatusFound)
return
}
fmt.Println("comment page!")
fmt.Println(comment)
fmt.Print(current_user_claims["email"])
h.Renderer.SupportSend(w, r, comment, current_user_claims)
}

View File

@ -5,10 +5,13 @@ import (
"encoding/json"
"fmt"
"net/http"
"sort"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
"git.coopgo.io/coopgo-platform/groups-management/storage"
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
@ -29,15 +32,58 @@ func (h *ApplicationHandler) VehiclesManagementOverview(w http.ResponseWriter, r
w.WriteHeader(http.StatusInternalServerError)
}
var vehicles = []any{}
vehicles := []fleetsstorage.Vehicle{}
bookings := []fleetsstorage.Booking{}
vehicles_map := map[string]fleetsstorage.Vehicle{}
for _, vehicle := range resp.Vehicles {
if filterVehicle(r, vehicle) {
v := vehicle.ToStorageType()
vehicles = append(vehicles, v)
vehicles_map[v.ID] = v
for _, b := range v.Bookings {
if b.Status() != fleetsstorage.StatusOld {
bookings = append(bookings, b)
}
}
}
}
h.Renderer.VehiclesManagementOverview(w, r, vehicles)
sort.Sort(sorting.VehiclesByLicencePlate(vehicles))
sort.Sort(sorting.BookingsByStartdate(bookings))
h.Renderer.VehiclesManagementOverview(w, r, vehicles, vehicles_map, bookings)
}
func (h *ApplicationHandler) VehiclesManagementBookingsList(w http.ResponseWriter, r *http.Request) {
//Get Vehicles
request := &fleets.GetVehiclesRequest{
Namespaces: []string{"parcoursmob"},
}
resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
}
bookings := []fleetsstorage.Booking{}
vehicles_map := map[string]fleetsstorage.Vehicle{}
for _, vehicle := range resp.Vehicles {
if filterVehicle(r, vehicle) {
v := vehicle.ToStorageType()
vehicles_map[v.ID] = v
bookings = append(bookings, v.Bookings...)
}
}
sort.Sort(sorting.BookingsByStartdate(bookings))
cacheid := uuid.NewString()
h.cache.PutWithTTL(cacheid, bookings, 1*time.Hour)
h.Renderer.VehiclesManagementBookingsList(w, r, vehicles_map, bookings, cacheid)
}
func (h *ApplicationHandler) VehiclesFleetAdd(w http.ResponseWriter, r *http.Request) {

View File

@ -4,9 +4,11 @@ import (
"context"
"fmt"
"net/http"
"sort"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/sorting"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
"git.coopgo.io/coopgo-platform/groups-management/storage"
@ -72,6 +74,8 @@ func (h ApplicationHandler) VehiclesSearch(w http.ResponseWriter, r *http.Reques
return
}
sort.Sort(sorting.BeneficiariesByName(accounts))
h.Renderer.VehiclesSearch(w, r, accounts, searched, vehicles, beneficiary, r.FormValue("startdate"), r.FormValue("enddate"))
}

View File

@ -3,8 +3,9 @@ package auth
import (
"git.coopgo.io/coopgo-apps/parcoursmob/renderer"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/cache"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
"git.coopgo.io/coopgo-platform/emailing"
"github.com/spf13/viper"
)
@ -13,10 +14,11 @@ type AuthHandler struct {
config *viper.Viper
services *services.ServicesHandler
Renderer *renderer.Renderer
cache *cache.CacheHandler
cache cache.CacheHandler
emailing *emailing.Mailer
}
func NewAuthHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, svc *services.ServicesHandler, cache *cache.CacheHandler) (*AuthHandler, error) {
func NewAuthHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, svc *services.ServicesHandler, cache cache.CacheHandler, emailing *emailing.Mailer) (*AuthHandler, error) {
templates_root := cfg.GetString("templates.root")
renderer := renderer.NewRenderer(cfg, templates_root)
return &AuthHandler{
@ -25,5 +27,6 @@ func NewAuthHandler(cfg *viper.Viper, idp *identification.IdentificationProvider
services: svc,
Renderer: renderer,
cache: cache,
emailing: emailing,
}, nil
}

View File

@ -0,0 +1,97 @@
package auth
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"net/http"
"time"
"git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
)
func (h *AuthHandler) LostPasswordInit(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
r.ParseForm()
email := r.FormValue("email")
if email != "" {
account, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &grpcapi.GetAccountUsernameRequest{
Username: email,
Namespace: "parcoursmob",
})
if err != nil {
fmt.Println(err)
http.Redirect(w, r, "/app/", http.StatusFound)
return
}
b := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
key := base64.RawURLEncoding.EncodeToString(b)
passwordretrieval := map[string]any{
"username": email,
"account_id": account.Account.Id,
"key": key,
}
h.cache.PutWithTTL("retrieve-password/"+key, passwordretrieval, 72*time.Hour)
if err := h.emailing.Send("auth.retrieve_password", email, passwordretrieval); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/app/", http.StatusFound)
}
}
h.Renderer.LostPasswordInit(w, r)
}
func (h *AuthHandler) LostPasswordRecover(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
key := r.FormValue("key")
recover, err := h.cache.Get("retrieve-password/" + key)
if err != nil {
fmt.Println(err)
h.Renderer.LostPasswordRecoverKO(w, r, key)
return
}
if r.Method == "POST" {
newpassword := r.FormValue("password")
if newpassword == "" {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Password is empty"))
return
}
_, err := h.services.GRPC.MobilityAccounts.ChangePassword(context.TODO(), &grpcapi.ChangePasswordRequest{
Id: recover.(map[string]any)["account_id"].(string),
Password: newpassword,
})
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
}
err = h.cache.Delete("retrieve-password/" + key)
if err != nil {
fmt.Println(err)
}
http.Redirect(w, r, "/app/", http.StatusFound)
}
h.Renderer.LostPasswordRecover(w, r, recover)
}

View File

@ -16,7 +16,7 @@ func (h *AuthHandler) Onboarding(w http.ResponseWriter, r *http.Request) {
onboarding, err := h.cache.Get("onboarding/" + key)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
h.Renderer.AuthOnboardingKO(w, r, key)
return
}
@ -72,6 +72,12 @@ func (h *AuthHandler) Onboarding(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = h.cache.Delete("onboarding/" + key)
if err != nil {
fmt.Println(err)
}
http.Redirect(w, r, "/app/", http.StatusFound)
}

21
main.go
View File

@ -11,8 +11,8 @@ import (
"git.coopgo.io/coopgo-apps/parcoursmob/handlers/auth"
"git.coopgo.io/coopgo-apps/parcoursmob/renderer"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/cache"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/identification"
cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
"github.com/gorilla/mux"
)
@ -33,12 +33,14 @@ func main() {
panic(err)
}
idp, err := identification.NewIdentificationProvider(cfg, svc)
kv, err := cache.NewKVHandler(cfg)
if err != nil {
panic(err)
}
cache, err := cache.NewCacheHandler(cfg)
filestorage, err := cache.NewFileStorage(cfg)
idp, err := identification.NewIdentificationProvider(cfg, svc, kv)
if err != nil {
panic(err)
}
@ -48,9 +50,9 @@ func main() {
panic(err)
}
apiHandler, _ := api.NewAPIHandler(cfg, idp, svc, cache)
applicationHandler, _ := application.NewApplicationHandler(cfg, svc, cache, emailing)
authHandler, _ := auth.NewAuthHandler(cfg, idp, svc, cache)
apiHandler, _ := api.NewAPIHandler(cfg, idp, svc, kv)
applicationHandler, _ := application.NewApplicationHandler(cfg, svc, kv, filestorage, emailing)
authHandler, _ := auth.NewAuthHandler(cfg, idp, svc, kv, emailing)
fmt.Println("Running", service_name, ":")
@ -59,6 +61,8 @@ func main() {
r.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir(templates_public_dir))))
r.HandleFunc("/auth/onboarding", authHandler.Onboarding)
r.HandleFunc("/auth/lost-password", authHandler.LostPasswordInit)
r.HandleFunc("/auth/lost-password/recover", authHandler.LostPasswordRecover)
r.HandleFunc("/auth/groups/", authHandler.Groups)
r.HandleFunc("/auth/groups/switch", authHandler.GroupSwitch)
r.HandleFunc("/", redirectApp)
@ -67,6 +71,7 @@ func main() {
api_router.HandleFunc("/", apiHandler.NotFound)
api_router.HandleFunc("/geo/autocomplete", apiHandler.GeoAutocomplete)
api_router.HandleFunc("/cache/{cacheid}", apiHandler.GetCache)
api_router.HandleFunc("/cache/{cacheid}/export", apiHandler.CacheExport)
api_router.HandleFunc("/oauth2/callback", apiHandler.OAuth2Callback)
application := r.PathPrefix("/app").Subrouter()
@ -75,6 +80,8 @@ func main() {
application.HandleFunc("/beneficiaries/create", applicationHandler.BeneficiaryCreate)
application.HandleFunc("/beneficiaries/{beneficiaryid}", applicationHandler.BeneficiaryDisplay)
application.HandleFunc("/beneficiaries/{beneficiaryid}/update", applicationHandler.BeneficiaryUpdate)
application.HandleFunc("/beneficiaries/{beneficiaryid}/documents", applicationHandler.BeneficiaryDocuments)
application.HandleFunc("/beneficiaries/{beneficiaryid}/documents/{document}", applicationHandler.BeneficiaryDocumentDownload)
application.HandleFunc("/beneficiaries/{beneficiaryid}/picture", applicationHandler.BeneficiaryPicture)
application.HandleFunc("/members/{beneficiaryid}/picture", applicationHandler.BeneficiaryPicture)
application.HandleFunc("/journeys/", applicationHandler.JourneysSearch)
@ -86,6 +93,7 @@ func main() {
application.HandleFunc("/vehicles-management/fleet/add", applicationHandler.VehiclesFleetAdd)
application.HandleFunc("/vehicles-management/fleet/{vehicleid}", applicationHandler.VehiclesFleetDisplay)
application.HandleFunc("/vehicles-management/fleet/{vehicleid}/update", applicationHandler.VehiclesFleetUpdate)
application.HandleFunc("/vehicles-management/bookings/", applicationHandler.VehiclesManagementBookingsList)
application.HandleFunc("/vehicles-management/bookings/{bookingid}", applicationHandler.VehicleManagementBookingDisplay)
application.HandleFunc("/agenda/", applicationHandler.AgendaHome)
application.HandleFunc("/agenda/create-event", applicationHandler.AgendaCreateEvent)
@ -109,6 +117,7 @@ func main() {
appAdmin.HandleFunc("/groups/", applicationHandler.AdministrationCreateGroup)
appAdmin.HandleFunc("/groups/{groupid}", applicationHandler.AdministrationGroupDisplay)
appAdmin.HandleFunc("/groups/{groupid}/invite-admin", applicationHandler.AdministrationGroupInviteAdmin)
appAdmin.HandleFunc("/groups/{groupid}/invite-member", applicationHandler.AdministrationGroupInviteMember)
//TODO Secure with Middleware checking for modules
fmt.Println("-> HTTP server listening on", address)

View File

@ -13,7 +13,7 @@ func (renderer *Renderer) AuthGroups(w http.ResponseWriter, r *http.Request, gro
}
func (renderer *Renderer) AuthOnboarding(w http.ResponseWriter, r *http.Request, key string, onboarding any) {
files := renderer.ThemeConfig.GetStringSlice("views.auth.onboarding.files")
files := renderer.ThemeConfig.GetStringSlice("views.auth.onboarding.form.files")
state := NewState(r, renderer.ThemeConfig, "")
state.ViewState = map[string]any{
"key": key,
@ -22,3 +22,41 @@ func (renderer *Renderer) AuthOnboarding(w http.ResponseWriter, r *http.Request,
renderer.RenderNoLayout("onboarding", w, r, files, state)
}
func (renderer *Renderer) AuthOnboardingKO(w http.ResponseWriter, r *http.Request, key string) {
files := renderer.ThemeConfig.GetStringSlice("views.auth.onboarding.ko.files")
state := NewState(r, renderer.ThemeConfig, "")
state.ViewState = map[string]any{
"key": key,
}
renderer.RenderNoLayout("onboarding", w, r, files, state)
}
func (renderer *Renderer) LostPasswordInit(w http.ResponseWriter, r *http.Request) {
files := renderer.ThemeConfig.GetStringSlice("views.auth.lost_password.init.files")
state := NewState(r, renderer.ThemeConfig, "")
state.ViewState = map[string]any{}
renderer.RenderNoLayout("lost_password_init", w, r, files, state)
}
func (renderer *Renderer) LostPasswordRecover(w http.ResponseWriter, r *http.Request, recover any) {
files := renderer.ThemeConfig.GetStringSlice("views.auth.lost_password.recover.form.files")
state := NewState(r, renderer.ThemeConfig, "")
state.ViewState = map[string]any{
"recover": recover,
}
renderer.RenderNoLayout("lost_password_recover", w, r, files, state)
}
func (renderer *Renderer) LostPasswordRecoverKO(w http.ResponseWriter, r *http.Request, key string) {
files := renderer.ThemeConfig.GetStringSlice("views.auth.lost_password.recover.ko.files")
state := NewState(r, renderer.ThemeConfig, "")
state.ViewState = map[string]any{
"key": key,
}
renderer.RenderNoLayout("lost_password_recover_ko", w, r, files, state)
}

View File

@ -4,14 +4,16 @@ import (
"encoding/json"
"html/template"
"net/http"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
)
const beneficiariesMenu = "beneficiaries"
type BeneficiariesListState struct {
Count int `json:"count"`
CacheId string `json:"cache_id"`
Beneficiaries []any `json:"beneficiaries"`
Count int `json:"count"`
CacheId string `json:"cache_id"`
Beneficiaries []mobilityaccountsstorage.Account `json:"beneficiaries"`
}
func (s BeneficiariesListState) JSON() template.JS {
@ -26,7 +28,7 @@ func (s BeneficiariesListState) JSONWithLimits(a int, b int) template.JS {
return s.JSON()
}
func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Request, accounts []any, cacheid string) {
func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Request, accounts []mobilityaccountsstorage.Account, cacheid string) {
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.list.files")
state := NewState(r, renderer.ThemeConfig, beneficiariesMenu)
@ -50,12 +52,15 @@ type BeneficiariesDisplayState struct {
Beneficiary any
}
func (renderer *Renderer) BeneficiaryDisplay(w http.ResponseWriter, r *http.Request, beneficiary any, bookings []any) {
func (renderer *Renderer) BeneficiaryDisplay(w http.ResponseWriter, r *http.Request, beneficiary any, bookings []any, beneficiaries_file_types []string, file_types_map map[string]string, documents any) {
files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.display.files")
state := NewState(r, renderer.ThemeConfig, beneficiariesMenu)
state.ViewState = map[string]any{
"beneficiary": beneficiary,
"bookings": bookings,
"beneficiary": beneficiary,
"bookings": bookings,
"beneficiaries_file_types": beneficiaries_file_types,
"file_types_map": file_types_map,
"documents": documents,
}
renderer.Render("beneficiaries_display", w, r, files, state)

View File

@ -5,11 +5,12 @@ import (
)
const commentMenu = "comment"
const commentsend = "sendComment"
//const commentsend = "sendComment"
func (renderer *Renderer) SupportSend(w http.ResponseWriter, r *http.Request, comment any, admins any) {
files := renderer.ThemeConfig.GetStringSlice("views.support.search.files")
files := renderer.ThemeConfig.GetStringSlice("views.support.request.files")
state := NewState(r, renderer.ThemeConfig, commentMenu)
state.ViewState = map[string]any{
"comment": comment,

View File

@ -1,14 +1,32 @@
package renderer
import "net/http"
import (
"net/http"
fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
)
const vehiclesmanagementMenu = "vehicles_management"
func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *http.Request, vehicles []any) {
func (renderer *Renderer) VehiclesManagementOverview(w http.ResponseWriter, r *http.Request, vehicles []fleetsstorage.Vehicle, vehicles_map map[string]fleetsstorage.Vehicle, bookings []fleetsstorage.Booking) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.overview.files")
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
"vehicles": vehicles,
"vehicles": vehicles,
"bookings": bookings,
"vehicles_map": vehicles_map,
}
renderer.Render("fleet overview", w, r, files, state)
}
func (renderer *Renderer) VehiclesManagementBookingsList(w http.ResponseWriter, r *http.Request, vehicles_map map[string]fleetsstorage.Vehicle, bookings []fleetsstorage.Booking, cacheid string) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles_management.bookings_list.files")
state := NewState(r, renderer.ThemeConfig, vehiclesmanagementMenu)
state.ViewState = map[string]any{
"bookings": bookings,
"vehicles_map": vehicles_map,
"cacheid": cacheid,
}
renderer.Render("fleet overview", w, r, files, state)

View File

@ -1,10 +1,14 @@
package renderer
import "net/http"
import (
"net/http"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
)
const vehiclesMenu = "vehicles"
func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request, beneficiaries []any, searched bool, vehicles []any, beneficiary any, startdate any, enddate any) {
func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request, beneficiaries []mobilityaccountsstorage.Account, searched bool, vehicles []any, beneficiary any, startdate any, enddate any) {
files := renderer.ThemeConfig.GetStringSlice("views.vehicles.search.files")
state := NewState(r, renderer.ThemeConfig, vehiclesMenu)
viewstate := map[string]any{

View File

@ -52,6 +52,10 @@ views:
- web/layouts/vehicles_management/_partials/bookings-list.html
- web/layouts/vehicles_management/_partials/vehicles-list.html
- web/layouts/vehicles_management/overview.html
bookings_list:
files:
- web/layouts/vehicles_management/_partials/bookings-list.html
- web/layouts/vehicles_management/bookings-list.html
fleet_add:
files:
- web/layouts/_partials/address_autocomplete.html
@ -94,7 +98,7 @@ views:
- web/layouts/journeys/_partials/journeys-public-transit.html
- web/layouts/journeys/search.html
support:
search:
request:
files:
- web/layouts/support/support.html
administration:
@ -119,9 +123,24 @@ views:
groups:
files:
- web/layouts/auth/groups.html
lost_password:
init:
files:
- web/layouts/auth/lost-password-init.html
recover:
form:
files:
- web/layouts/auth/lost-password-recover.html
ko:
files:
- web/layouts/auth/lost-password-recover-ko.html
onboarding:
files:
- web/layouts/auth/onboarding.html
form:
files:
- web/layouts/auth/onboarding.html
ko:
files:
- web/layouts/auth/onboarding-ko.html
icons:
svg:
@ -131,12 +150,16 @@ icons:
hero:outline/calendar: <svg xmlns="http://www.w3.org/2000/svg" class="%s" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
hero:outline/chevron-right: <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%s"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /></svg>
hero:outline/cog: <svg xmlns="http://www.w3.org/2000/svg" class="%s" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
hero:outline/document-arrow-down: <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%s"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m.75 12l3 3m0 0l3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
hero:outline/document-text: <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%s"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
hero:outline/folder-plus: <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%s"><path stroke-linecap="round" stroke-linejoin="round" d="M12 10.5v6m3-3H9m4.06-7.19l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" /></svg>
hero:outline/home: <svg xmlns="http://www.w3.org/2000/svg" class="%s" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>
hero:outline/map: <svg xmlns="http://www.w3.org/2000/svg" class="%s" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" /></svg>
hero:outline/office-building: <svg xmlns="http://www.w3.org/2000/svg" class="%s" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>
hero:outline/paper-clip: <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%s"><path stroke-linecap="round" stroke-linejoin="round" d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13" /></svg>
hero:outline/plus-circle: <svg xmlns="http://www.w3.org/2000/svg" class="%s" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
hero:outline/shield-check: <svg xmlns="http://www.w3.org/2000/svg" class="%s" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
hero:outline/table-cells: <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%s"><path stroke-linecap="round" stroke-linejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 01-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0112 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 1.5v-1.5m0 0c0-.621.504-1.125 1.125-1.125m0 0h7.5" /></svg>
hero:outline/user-group: <svg xmlns="http://www.w3.org/2000/svg" class="%s" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
hero:outline/x: <svg class="%s text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
hero:solid/chevron-right: <svg class="%s text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" /></svg>
@ -170,8 +193,15 @@ emails:
files:
- emails/layout.html
- emails/onboarding/existing-member.html
support_email:
subject: PARCOURMOB - Vous avez reçu un commentaire
auth:
retrieve_password:
subject: PAROURSMOB - RĂ©initialisez votre mot de passe
files:
- emails/layout.html
- emails/auth/retrieve-password.html
support:
request:
subject: PARCOURMOB - Demande de support
files:
- emails/layout.html
- emails/onboarding/support_emailing.html

View File

@ -0,0 +1,5 @@
{{define "content"}}
<p>Bonjour,</p>
<p>Vous avez demandé à réinitialiser votre mot de passe pour <b>{{.username}}</b></p>
<p>Pour créer votre nouveau mot de passe, cliquez sur le lien suivant : <a href="http://localhost:9000/auth/lost-password/recover?key={{.key}}">http://localhost:9000/auth/lost-password/recover?key={{.key}}</a></p>
{{end}}

View File

@ -1,5 +1,5 @@
{{define "content"}}
<p>Vous avez été ajouté comme administrateur de l'organisation {{.group}} sur PARCOURSMOB.</p>
<p>Vous devez créer votre compte pour y accéder.</p>
<p>Pour créer votre compte PARCOURSMOB, cliquez sur : <a href="http://localhost:9000/auth/onboarding?key={{.key}}">http://localhost:9000/onboarding?key={{.key}}</a></p>
<p>Pour créer votre compte PARCOURSMOB, cliquez sur : <a href="http://localhost:9000/auth/onboarding?key={{.key}}">http://localhost:9000/auth/onboarding?key={{.key}}</a></p>
{{end}}

View File

@ -1,5 +1,5 @@
{{define "content"}}
<p>Vous avez été ajouté à l'organisation {{.group}} sur PARCOURSMOB.</p>
<p>Vous devez créer votre compte pour y accéder.</p>
<p>Pour créer votre compte PARCOURSMOB, cliquez sur : <a href="http://localhost:9000/auth/onboarding?key={{.key}}">http://localhost:9000/onboarding?key={{.key}}</a></p>
<p>Pour créer votre compte PARCOURSMOB, cliquez sur : <a href="http://localhost:9000/auth/onboarding?key={{.key}}">http://localhost:9000/auth/onboarding?key={{.key}}</a></p>
{{end}}

View File

@ -3,7 +3,7 @@
<div class="col-span-6 relative" x-data="{
input: {{if .Address}}'{{.Address.Properties.label}}'{{else}}null{{end}},
address: null,
address: {{if .Address}}JSON.stringify({{printf "%s" .Address.MarshalJSON}}){{else}}null{{end}},
addressObject: {{if .Address}}{{printf "%s" .Address.MarshalJSON}}{{else}}null{{end}},
responselength: 0,
async autocomplete() {

View File

@ -0,0 +1,31 @@
{{define "documents_list"}}
<div class="-mx-4 mt-10 ring-1 ring-gray-300 sm:-mx-6 md:mx-0 md:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Type</th>
<th scope="col" class="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">Nom du document</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="relative py-4 pl-4 sm:pl-6 pr-3 text-sm">
<div class="font-medium text-gray-900">
<span class="bg-co-blue text-xs text-white rounded-xl p-1 mr-2">{{index $.ViewState.file_types_map .Metadata.Type}}</span>
</div>
</td>
<td class="px-3 py-3.5 text-sm text-gray-500 lg:table-cell">{{.Metadata.Name}}</td>
<td class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right text-sm font-medium">
<button type="button" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-30">Télécharger<span class="sr-only"> le fichier</span></button>
</td>
</tr>
<!-- More plans... -->
</tbody>
</table>
</div>
{{end}}

View File

@ -42,6 +42,20 @@
</form>
</div>
{{template "groups_members" .}}
<div class="px-2 py-4">
<form class="flex" method="POST" action="/app/administration/groups/{{.ViewState.group.ID}}/invite-member">
<div class="pr-2 flex-1">
<input type="text" name="username" id="username"
class="mt-1 border-gray-300 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-2xl"
placeholder="Email">
</div>
<button class="px-1 py-1 border border-transparent text-sm font-medium rounded-2xl shadow-sm text-white bg-co-blue hover:bg-co-blue focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-co-blue">Ajouter un membre</button>
</form>
</div>
</div>
</section>
</div>

View File

@ -0,0 +1,40 @@
{{define "main"}}
<html class="h-full bg-gray-50">
<head>
<title>PARCOURSMOB - Identification</title>
<link rel="stylesheet" href="http://localhost:9000/public/css/main.css" />
</head>
<body class="h-full">
<form method="post">
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<!-- <img class="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company"> -->
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">RĂ©initialiser votre mot de passe PARCOURSMOB</h2>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Votre email</label>
<div class="mt-1">
<input id="email" name="email" type="text" autocomplete="email" required class="block w-full appearance-none rounded-2xl border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-co-blue focus:outline-none focus:ring-co-blue sm:text-sm">
</div>
</div>
<p class="p-4 text-gray-500 text-xs">Si votre compte existe, vous allez recevoir un mot de passe par email contenant un lien pour réinitialiser votre mot de passe. Celui-ci sera actif pendant 72h.</p>
<div>
<button type="submit" class="mt-2 flex w-full justify-center rounded-2xl border border-transparent bg-co-blue py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-co-blue focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2">Recevoir un lien de réinitialisation</button>
</div>
</div>
</div>
</div>
</form>
</body>
</html>
{{end}}

View File

@ -0,0 +1,26 @@
{{define "main"}}
<html class="h-full bg-gray-50">
<head>
<title>PARCOURSMOB - Identification</title>
<link rel="stylesheet" href="http://localhost:9000/public/css/main.css" />
</head>
<body class="h-full">
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<!-- <img class="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company"> -->
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">RĂ©initialiser votre mot de passe PARCOURSMOB</h2>
<p class="p-12 text-gray-500 text-center text-md">Ce lien de réinitialisation n'est plus actif. Vous l'avez déjà utilisé ou il a expiré. Vous pouvez redemander un nouveau mot de passe ou réessayer de vous connecter directement à PARCOURSMOB.</p>
<div>
<a href="/">
<button class="mt-2 flex w-full justify-center rounded-2xl border border-transparent bg-co-blue py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-co-blue focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2">Se connecter Ă  PARCOURSMOB</button>
</a>
</div>
</div>
</div>
</body>
</html>
{{end}}

View File

@ -0,0 +1,41 @@
{{define "main"}}
<html class="h-full bg-gray-50">
<head>
<title>PARCOURSMOB - Identification</title>
<link rel="stylesheet" href="http://localhost:9000/public/css/main.css" />
</head>
<body class="h-full">
<form method="post">
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<!-- <img class="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company"> -->
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">RĂ©initialisez votre mot de passe PARCOURSMOB</h2>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<p class="p-4 text-gray-500 text-xs">Vous avez demandé à réinitialiser votre mot de passe pour <b>{{.ViewState.recover.username}}</b></p>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Votre nouveau mot de passe</label>
<div class="mt-1">
<input id="password" name="password" type="password" required class="block w-full appearance-none rounded-2xl border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-co-blue focus:outline-none focus:ring-co-blue sm:text-sm">
</div>
</div>
<div>
<button type="submit" class="mt-2 flex w-full justify-center rounded-2xl border border-transparent bg-co-blue py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-co-blue focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2">RĂ©initialiser</button>
</div>
</div>
</div>
</div>
</form>
</body>
</html>
{{end}}

View File

@ -0,0 +1,26 @@
{{define "main"}}
<html class="h-full bg-gray-50">
<head>
<title>PARCOURSMOB - Identification</title>
<link rel="stylesheet" href="http://localhost:9000/public/css/main.css" />
</head>
<body class="h-full">
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<!-- <img class="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company"> -->
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">Inscription Ă  PARCOURSMOB</h2>
<p class="p-12 text-gray-500 text-center text-md">Ce lien d'inscription n'est plus actif. Vous avez peut être déjà créé votre compte. Si ce n'est pas le cas, le lien a pu expirer : veuillez en demander un nouveau à l'administrateur PARCOURSMOB de votre structure.</p>
<div>
<a href="/">
<button class="mt-2 flex w-full justify-center rounded-2xl border border-transparent bg-co-blue py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-co-blue focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2">Se connecter Ă  PARCOURSMOB</button>
</a>
</div>
</div>
</div>
</body>
</html>
{{end}}

View File

@ -1,5 +1,135 @@
{{define "beneficiary_files"}}
<div class="px-4 py-6 sm:px-6">
TODO Fichiers
<div class="px-4 py-6 sm:px-6"
x-data="{
fields: {
name: null,
type: null,
file: null,
},
rules: {
name: ['required'],
type: ['required'],
file: ['required'],
},
formValidation: {
valid: false,
fields: {
name: {valid: null},
type: {valid: null},
file: {valid: null},
}
},
isFormValid: true,
validate() {
this.formValidation = Iodine.assert(this.fields, this.rules)
},
validateField(field) {
this.formValidation.fields[field] = Iodine.assert(this.fields[field], this.rules[field])
},
submit(event) {
this.validate()
if(!this.formValidation.valid) {
this.isFormValid = false
event.preventDefault()
}
return this.formValidation.valid
}
}">
{{if eq (len .ViewState.documents) 0}}
<p class="p-12 text-gray-500 text-center text-md">Aucun document</p>
{{end}}
{{if gt (len .ViewState.documents) 0}}
<div class="-mx-4 mb-10 ring-1 ring-gray-300 sm:-mx-6 md:mx-0 md:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Type</th>
<th scope="col" class="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">Nom du document</th>
<th scope="col" class="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell">Ajouté le</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
{{range .ViewState.documents}}
<tr>
<td class="relative py-4 pl-4 sm:pl-6 pr-3 text-sm">
<div class="font-medium text-gray-900">
<span class="bg-co-blue text-xs text-white rounded-xl p-1 mr-2">{{index $.ViewState.file_types_map .Metadata.Type}}</span>
</div>
</td>
<td class="px-3 py-3.5 text-sm text-gray-900 lg:table-cell">{{.Metadata.Name}}</td>
<td class="px-3 py-3.5 text-sm text-gray-500 lg:table-cell">{{.LastModified.Format "02/01/2006"}}</td>
<td class="relative py-3.5 pl-3 pr-4 sm:pr-6 text-right text-sm font-medium">
<a href="/app/beneficiaries/{{$.ViewState.beneficiary.ID}}/documents/{{.FileName}}" target="_blank">
<button type="button" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-30">Voir<span class="sr-only"> le document</span></button>
</a>
</td>
</tr>
{{end}}
<!-- More plans... -->
</tbody>
</table>
</div>
{{end}}
<h3 class="text-lg">Ajouter un document</h3>
<form method="POST" action="/app/beneficiaries/e7616eac-4a87-4396-a505-23e0421b9c4c/documents" @submit="submit" enctype="multipart/form-data">
<div class="md:grid md:grid-cols-6 p-2">
<div class="sm:col-span-2">
<label for="type" class="block text-sm font-medium text-gray-700">Type</label>
<select id="type" name="type" class="mt-1 block w-full rounded-l-2xl border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
x-model="fields.type" @blur="validateField('type')"
:class="formValidation.fields.type.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
<option value="" selected>SĂ©lectionner un type</option>
{{range .ViewState.beneficiaries_file_types}}
<option value="{{.}}">{{index $.ViewState.file_types_map .}}</option>
{{end}}
</select>
</div>
<div class="sm:col-span-4">
<label for="name" class="block text-sm font-medium text-gray-700">Nom</label>
<input type="text" name="name" id="name"
placeholder="Nom du fichier"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-r-2xl"
x-model="fields.name" @blur="validateField('name')"
:class="formValidation.fields.name.valid == false ? 'border-co-red border-2' : 'border-gray-300'" />
</div>
<div class="sm:col-span-6 mt-4">
<label for="cover-photo" class="block text-sm font-medium text-gray-700">Téléchargement</label>
<div class="mt-1 flex justify-center rounded-md border-2 border-dashed px-6 pt-5 pb-6"
x-on:drop="console.log('toto')"
:class="formValidation.fields.file.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
<div class="space-y-1 text-center">
{{.IconSet.Icon "hero:outline/folder-plus" "mx-auto h-12 w-12 text-gray-400"}}
<div class="flex text-sm text-gray-600">
<label for="file-upload" class="relative cursor-pointer rounded-md bg-white font-medium text-co-blue focus-within:outline-none focus-within:ring-2 focus-within:ring-co-blue focus-within:ring-offset-2 hover:text-co-blue">
<span>SĂ©lectionnez un fichier </span>
<input id="file-upload" name="file-upload" type="file" class="sr-only"
x-model="fields.file" @blur="validateField('file')">
</label>
<!-- <p class="pl-1">ou glissez-déposez</p> -->
</div>
<p class="text-xs text-gray-500">Jusqu'Ă  10MB</p>
<p class="text-co-blue p-2" x-text="fields.file" x-if="fields.file"></p>
</div>
</div>
</div>
</div>
<button type="submit"
class="rounded-2xl border border-transparent bg-co-blue px-4 py-2 my-4 mt-8 w-full text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2 sm:w-auto">
Ajouter le document
</button>
</form>
</div>
{{end}}

View File

@ -11,14 +11,16 @@
last_name: null,
email: null,
phone_number: null,
birthdate: null
birthdate: null,
file_number: null
},
rules: {
first_name: ['required'],
last_name: ['required'],
email: ['required', 'email'],
phone_number: ['required', 'regexMatch:^((\\+)33|0)[1-9](\\d{2}){4}$'],
birthdate: ['optional'],
birthdate: ['required'],
file_number: ['optional'],
},
formValidation: {
valid: false,
@ -28,6 +30,7 @@
email: {valid: null},
phone_number: {valid: null},
birthdate: {valid: null},
file_number: {valid: null},
}
},
isFormValid: true,
@ -72,7 +75,7 @@
:class="formValidation.fields.last_name.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="col-span-6 sm:col-span-4">
<div class="col-span-6 sm:col-span-3">
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input type="text" name="email" id="email" autocomplete="email"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-2xl"
@ -80,7 +83,7 @@
:class="formValidation.fields.email.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="col-span-6 sm:col-span-4">
<div class="col-span-6 sm:col-span-3">
<label for="phone_number" class="block text-sm font-medium text-gray-700">Numéro de
téléphone</label>
<input type="text" name="phone_number" id="phone_number" autocomplete="phone" placeholder="+33612345678"
@ -88,6 +91,15 @@
x-model="fields.phone_number" @blur="validateField('phone_number')"
:class="formValidation.fields.phone_number.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="col-span-6 sm:col-span-3">
<label for="birthdate" class="block text-sm font-medium text-gray-700">Date de
naissance</label>
<input type="date" name="birthdate" id="birthdate" autocomplete="birthdate" placeholder="JJ/MM/AAAA"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-2xl"
x-model="fields.birthdate" @blur="validateField('birthdate')"
:class="formValidation.fields.birthdate.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
</div>
</div>
</div>
@ -103,12 +115,11 @@
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<label for="birthdate" class="block text-sm font-medium text-gray-700">Date de
naissance</label>
<input type="date" name="birthdate" id="birthdate" autocomplete="birthdate" placeholder="JJ/MM/AAAA"
<label for="file_number" class="block text-sm font-medium text-gray-700">Numéro de dossier (allocataire, ...)</label>
<input type="text" name="file_number" id="file_number" placeholder=""
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-2xl"
x-model="fields.birthdate" @blur="validateField('birthdate')"
:class="formValidation.fields.birthdate.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
x-model="fields.file_number" @blur="validateField('file_number')"
:class="formValidation.fields.file_number.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="col-span-6 sm:col-span-3">

View File

@ -75,7 +75,7 @@
</section>
<section aria-labelledby="functionalities-title" x-data="{
tab: 'vehicles',
tab: 'documents',
to(event) {
this.tab = event.target.value
}
@ -91,9 +91,11 @@
<option value="journeys">DĂ©placements</option>
<option value="vehicles">VĂ©hicules</option>
<option value="events">Dispositifs</option>
<option value="files">Documents</option>
<option value="documents">Documents</option>
</select>
</div>
<div class="hidden sm:block">
@ -120,10 +122,10 @@
:class="tab == 'events' ? 'border-co-blue text-co-blue' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'">
Dispositifs </a>
<!-- <a href="#" @click="tab = 'files'"
<a href="#" @click="tab = 'documents'"
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
:class="tab == 'files' ? 'border-co-blue text-co-blue' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'">
Documents </a> -->
:class="tab == 'documents' ? 'border-co-blue text-co-blue' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'">
Documents </a>
</nav>
</div>
</div>
@ -132,7 +134,7 @@
<div x-show="tab == 'journeys'">{{template "beneficiary_journeys" .}}</div>
<div x-show="tab == 'vehicles'">{{template "beneficiary_vehicles" .}}</div>
<div x-show="tab == 'events'">{{template "beneficiary_events" .}}</div>
<div x-show="tab == 'files'">{{template "beneficiary_files" .}}</div>
<div x-show="tab == 'documents'">{{template "beneficiary_files" .}}</div>
<div x-show="tab == 'notes'">{{template "beneficiary_notes" .}}</div>
</div>
</div>

View File

@ -7,6 +7,13 @@
<p class="mt-2 text-sm text-gray-700"></p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<a href="/api/cache/{{.ViewState.CacheId}}/export">
<button type="button"
class="inline-flex items-center justify-center bg-white hover:bg-gray-50 border-gray-300 border px-4 py-2 text-gray-700 flex items-center text-sm rounded-2xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">
{{$.IconSet.Icon "hero:outline/document-arrow-down" "h-5 w-5 mr-3"}}
Exporter
</button>
</a>
<a href="/app/beneficiaries/create">
<button type="button"
class="inline-flex items-center justify-center rounded-2xl border border-transparent bg-co-blue px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-ci-blue focus:ring-offset-2 sm:w-auto">

View File

@ -12,6 +12,7 @@
email: '{{ .ViewState.Data.email }}',
phone_number: '{{ .ViewState.Data.phone_number }}',
birthdate: {{if .ViewState.Data.birthdate}}'{{ (timeFrom .ViewState.Data.birthdate).Format "2006-01-02" }}'{{else}}null{{end}},
file_number: '{{ .ViewState.Data.file_number }}',
gender: {{.ViewState.Data.gender}}
},
rules: {
@ -19,7 +20,8 @@
last_name: ['required'],
email: ['required', 'email'],
phone_number: ['required', 'regexMatch:^((\\+)33|0)[1-9](\\d{2}){4}$'],
birthdate: ['optional'],
birthdate: ['required'],
file_number: ['optional'],
},
formValidation: {
valid: false,
@ -29,6 +31,7 @@
email: {valid: null},
phone_number: {valid: null},
birthdate: {valid: null},
file_number: {valid: null},
}
},
isFormValid: true,
@ -73,7 +76,7 @@
:class="formValidation.fields.last_name.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="col-span-6 sm:col-span-4">
<div class="col-span-6 sm:col-span-3">
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input type="text" name="email" id="email" autocomplete="email"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-2xl"
@ -81,7 +84,7 @@
:class="formValidation.fields.email.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="col-span-6 sm:col-span-4">
<div class="col-span-6 sm:col-span-3">
<label for="phone_number" class="block text-sm font-medium text-gray-700">Numéro de
téléphone</label>
<input type="text" name="phone_number" id="phone_number" autocomplete="phone" placeholder="+33612345678"
@ -89,6 +92,15 @@
x-model="fields.phone_number" @blur="validateField('phone_number')"
:class="formValidation.fields.phone_number.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="col-span-6 sm:col-span-3">
<label for="birthdate" class="block text-sm font-medium text-gray-700">Date de
naissance</label>
<input type="date" name="birthdate" id="birthdate" autocomplete="birthdate" placeholder="JJ/MM/AAAA"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-2xl"
x-model="fields.birthdate" @blur="validateField('birthdate')"
:class="formValidation.fields.birthdate.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
</div>
</div>
</div>
@ -104,12 +116,11 @@
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<label for="birthdate" class="block text-sm font-medium text-gray-700">Date de
naissance</label>
<input type="date" name="birthdate" id="birthdate" autocomplete="birthdate" placeholder="JJ/MM/AAAA"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-2xl"
x-model="fields.birthdate" @blur="validateField('birthdate')"
:class="formValidation.fields.birthdate.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
<label for="file_number" class="block text-sm font-medium text-gray-700">Numéro de dossier (allocataire, ...)</label>
<input type="text" name="file_number" id="file_number"
class="mt-1 focus:ring-co-blue focus:border-co-blue block w-full shadow-sm sm:text-sm rounded-2xl"
x-model="fields.file_number" @blur="validateField('file_number')"
:class="formValidation.fields.file_number.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
</div>
<div class="col-span-6 sm:col-span-3">

View File

@ -53,7 +53,7 @@
{{end}}
{{ if gt (len .ViewState.journeys.Journeys) 0}}
<div class="px-4 pt-4 flex text-sm text-grey-900 font-bold border-t-2">
<div class="flex-1">
{{.IconSet.Icon "tabler-icons:bus" "h-6 w-6 inline-flex mr-4"}}
@ -76,7 +76,7 @@
{{end}}
{{if eq .Type "public_transport"}}
<span class="ml-2 rounded-xl px-2 py-1 bg-co-blue flex items-center justify-center ring-8 ring-white text-sm text-white whitespace-nowrap">
{{if eq .Display.Network "Antibes"}}Envibus{{else}}{{.Display.Network}}{{end}} Ligne {{.Display.Label}}
{{if eq .Display.Network "Antibes - Envibus"}}Envibus{{else}}{{.Display.Network}}{{end}} Ligne {{.Display.Label}}
</span>
{{$.IconSet.Icon "hero:outline/chevron-right" "h-3 w-3 stroke-gray-800 m-2"}}
{{end}}
@ -88,6 +88,8 @@
<button class="rounded-xl text-md px-4 py-1 bg-gray-200 text-co-blue" @click="tab = 'public-transit'">{{ len .ViewState.journeys.Journeys}} solutions en transports en commun : les voir toutes</button>
</div>
{{end}}
<!--VEHICLES-->
<div class="px-4 pt-16 flex text-sm text-grey-900 border-t-2">
<div class="flex-1">

View File

@ -1,5 +1,9 @@
{{define "journeys_carpool"}}
{{ if eq (len .ViewState.carpools) 0}}
<p class="p-12 text-gray-500 text-center text-md">Aucun covoiturage disponible pour ce trajet.</p>
{{end}}
{{$first := true}}
{{range .ViewState.carpools}}
{{if $first}}

View File

@ -1,5 +1,9 @@
{{define "journeys_public_transit"}}
{{ if eq (len .ViewState.journeys.Journeys) 0}}
<p class="p-12 text-gray-500 text-center text-md">Aucun transport en commun pour ce trajet.</p>
{{end}}
{{$first := true}}
{{range .ViewState.journeys.Journeys}}
{{if $first}}
@ -51,7 +55,7 @@
</div>
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div>
<p class="text-md text-gray-500">{{if eq .Display.Network "Antibes"}}Envibus{{else}}{{.Display.Network}}{{end}} <a href="#" class="font-medium text-gray-900">Ligne {{.Display.Label}}</a></p>
<p class="text-md text-gray-500">{{if eq .Display.Network "Antibes - Envibus"}}Envibus{{else}}{{.Display.Network}}{{end}} <a href="#" class="font-medium text-gray-900">Ligne {{.Display.Label}}</a></p>
</div>
</div>
</div>

View File

@ -32,7 +32,7 @@
</div>
<div class="flex-shrink-0 flex items-center px-4">
<img class="h-8 w-auto" src="/public/images/parcoursmob_logo_whitered.svg" alt="PARCOURSMOB">
<img class="h-8 w-auto" src="/public/images/main_logo.svg" alt="PARCOURSMOB">
</div>
<div class="mt-5 flex-1 h-0 overflow-y-auto">

View File

@ -1,15 +1,18 @@
{{define "content"}}
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 space-y-6">
<h1 class="text-2xl font-semibold text-gray-900">Contact</h1>
<h1 class="text-2xl font-semibold text-gray-900">Demande de support</h1>
<div class="bg-white py-2 px-4 shadow sm:rounded-lg sm:px-10">
<p class="text-sm text-gray-600 p-4">
Le support PARCOURSMOB est ouvert les jours ouvrés de 9h à 18h. Vous pouvez également nous joindre par email à <b class="text-co-blue"><a href="mailto:support@parcoursmob.fr">support@parcoursmob.fr</a></b>, par exemple pour nous envoyez des copies d'écran du problème que vous rencontrez.
</p>
<form action="" method="POST">
<div class="mb-4 w-full bg-gray-50 rounded-lg border border-gray-200 dark:bg-gray-700 dark:border-gray-600">
<div class="py-2 px-4 bg-white rounded-t-lg dark:bg-gray-800">
<div class="mb-4 w-full bg-gray-50 rounded-3xl border border-gray-200 dark:bg-gray-700 dark:border-gray-600">
<div class="py-2 px-4 rounded-3xl bg-white dark:bg-gray-800">
<label class="sr-only">Votre message</label>
<textarea name="comment" rows="4" class="block w-full resize-none border-0 border-b border-transparent p-0 pb-2 focus:border-indigo-600 focus:ring-0 sm:text-sm" placeholder="Votre message..." required></textarea>
<textarea name="comment" rows="4" class="block w-full resize-none border-0 border-b border-transparent p-0 pb-2 focus:border-co-blue focus:ring-0 sm:text-sm" placeholder="Votre message..." required></textarea>
</div>
<div class="flex justify-center items-center py-2 px-3 border-t dark:border-gray-600">
<button type="submit" value="send message" class="px-2 py-2 border border-transparent text-sm font-medium rounded-2xl shadow-sm text-white bg-co-blue hover:bg-co-blue focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-co-blue">

View File

@ -10,7 +10,7 @@
<a href="/app/vehicles/bookings/">
<button type="button"
class="inline-flex items-center justify-center rounded-2xl border border-transparent bg-co-blue px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2 sm:w-auto">
Voir les prêts de véhicules
Voir les mises Ă  disposition
</button>
</a>
</div>
@ -23,14 +23,15 @@
<div class="border-t border-gray-200 px-4 py-5 sm:px-6">
<form method="GET">
<div x-data="{
text: '{{if .ViewState.search}}{{.ViewState.search.beneficiary.Data.first_name}} {{.ViewState.search.beneficiary.Data.last_name}}{{end}}',
beneficiariesListOpen: false,
beneficiaries: {{json .ViewState.beneficiaries}},
filteredBeneficiaries: (text) => {
if(text=='') return beneficiaries
return this.beneficiaries.filter(b => b['data']['first_name'].includes(text) || b['data']['last_name'].includes(text))
filteredBeneficiaries: (beneficiaries, text) => {
if(text=='') {
return beneficiaries
}
return beneficiaries.filter(b => b['data']['first_name'].toLowerCase().includes(text.toLowerCase()) || b['data']['last_name'].toLowerCase().includes(text.toLowerCase()))
},
fields: {
beneficiaryid: {{if .ViewState.search}}'{{.ViewState.search.beneficiary.ID}}'{{else}}null{{end}},
@ -45,7 +46,7 @@
<input type="hidden" name="beneficiaryid" x-model="fields.beneficiaryid">
<label for="combobox" class="block text-sm font-medium text-gray-700">Bénéficiaire</label>
<div class="relative mt-1 mb-4">
<input @focus="beneficiariesListOpen = true" x-model="text" id="combobox" type="text" class="w-full rounded-2xl border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-co-blue focus:outline-none focus:ring-1 focus:ring-co-blue sm:text-sm" role="combobox" aria-controls="options" aria-expanded="false">
<input autocomplete="off" @focus="beneficiariesListOpen = true" x-model="text" id="combobox" type="text" class="w-full rounded-2xl border border-gray-300 bg-white py-2 pl-3 pr-12 shadow-sm focus:border-co-blue focus:outline-none focus:ring-1 focus:ring-co-blue sm:text-sm" role="combobox" aria-controls="options" aria-expanded="false">
<button @click="beneficiariesListOpen = ! beneficiariesListOpen" type="button" class="absolute inset-y-0 right-0 flex items-center rounded-r-2xl px-2 focus:outline-none">
<!-- Heroicon name: solid/selector -->
@ -60,7 +61,7 @@
Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
-->
<template x-for="beneficiary in beneficiaries">
<template x-for="beneficiary in filteredBeneficiaries(beneficiaries, text)">
<li @click="selectbeneficiary(beneficiary)" class="relative cursor-default hover:bg-gray-100 select-none py-2 pl-3 pr-9 text-gray-900" id="option-0" role="option" tabindex="-1">
<!-- Selected: "font-semibold" -->
<span class="truncate" x-text="beneficiary.data.first_name"></span> <span class="truncate" x-text="beneficiary.data.last_name"></span>

View File

@ -7,22 +7,19 @@
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<!-- <th scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">
<th scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 text-center">
Statut
</th> -->
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Type
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Immatriculation
Numéro (Immat / Bicycode)
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Beneficiaire
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Gestionnaire
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6">
Dates
</th>
@ -32,29 +29,35 @@
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{{range .ViewState.vehicles}}
{{$vehicle := .}}
{{range .Bookings}}
{{range .ViewState.bookings}}
<tr>
<!-- <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >aa</div>
</td> -->
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >Voiture</div>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6 text-center">
{{if eq .Status 1 }}
<span class="p-1 bg-co-blue text-white text-xs font-bold rounded-xl" >
A venir
</span>
{{end}}
{{if eq .Status 0 }}
<span class="p-1 bg-co-green text-white text-xs font-bold rounded-xl" >
En cours
</span>
{{end}}
{{if eq .Status -1 }}
<span class="p-1 bg-co-red text-white text-xs font-bold rounded-xl" >
Terminé
</span>
{{end}}
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >{{$vehicle.Data.licence_plate}}</div>
<div class="text-gray-900" >{{ (index $.ViewState.vehicles_map .Vehicleid).Type }}</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >{{ (index $.ViewState.vehicles_map .Vehicleid).Data.licence_plate }}</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" ><img class="h-6 w-6 rounded-co"
src="/app/beneficiaries/{{.Driver}}/picture" alt=""></div>
</td>
<!-- <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >aa</div>
</td> -->
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >COOPGO</div>
</td>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div class="text-gray-900" >Du {{(timeFrom .Startdate).Format "02/01/2006"}} au {{(timeFrom .Enddate).Format "02/01/2006"}}</div>
</td>
@ -64,7 +67,6 @@
</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>

View File

@ -0,0 +1,25 @@
{{define "content"}}
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<h1 class="text-2xl font-semibold text-gray-900">Historique des réservations de véhicules</h1>
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<a href="/api/cache/{{.ViewState.cacheid}}/export">
<button type="button"
class="inline-flex items-center justify-center bg-white hover:bg-gray-50 border-gray-300 border px-4 py-2 text-gray-700 flex items-center text-sm rounded-2xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-co-blue">
{{$.IconSet.Icon "hero:outline/document-arrow-down" "h-5 w-5 mr-3"}}
Exporter
</button>
</a>
</div>
</div>
</div>
{{template "bookings_list" .}}
{{end}}

View File

@ -2,17 +2,31 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<h1 class="text-2xl font-semibold text-gray-900">Gestion des véhicules et réservations</h1>
<h2 class="text-xl font-semibold text-gray-600 pt-8">RĂ©servations</h2>
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h2 class="text-xl font-semibold text-gray-600 pt-8">RĂ©servations en cours et Ă  venir</h2>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<a href="/app/vehicles-management/bookings/">
<button type="button"
class="inline-flex items-center justify-center rounded-2xl border border-transparent bg-co-blue px-4 py-2 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-co-blue focus:ring-offset-2 sm:w-auto">
{{$.IconSet.Icon "hero:outline/table-cells" "h-5 w-5 mr-3"}}
Historique
</button>
</a>
</div>
</div>
</div>
{{template "bookings_list" .}}
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<h2 class="text-xl font-semibold text-gray-600 pt-8">VĂ©hicules</h2>
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 mt-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<p class="mt-2 text-sm text-gray-700"></p>
<h2 class="text-xl font-semibold text-gray-600 pt-8">VĂ©hicules</h2>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<a href="/app/vehicles-management/fleet/add">

View File

@ -806,6 +806,18 @@ html {
left: 1rem;
}
.left-6 {
left: 1.5rem;
}
.-top-px {
top: -1px;
}
.right-6 {
right: 1.5rem;
}
.z-40 {
z-index: 40;
}
@ -993,6 +1005,10 @@ html {
margin-right: -0.25rem;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.block {
display: block;
}
@ -1069,6 +1085,10 @@ html {
height: 0.75rem;
}
.h-px {
height: 1px;
}
.max-h-60 {
max-height: 15rem;
}
@ -1239,6 +1259,10 @@ html {
user-select: none;
}
.resize-none {
resize: none;
}
.appearance-none {
-webkit-appearance: none;
-moz-appearance: none;
@ -1507,9 +1531,9 @@ html {
border-bottom-left-radius: 1rem;
}
.rounded-l {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
.rounded-t-lg {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
.border {
@ -1520,6 +1544,10 @@ html {
border-width: 2px;
}
.border-0 {
border-width: 0px;
}
.border-r {
border-right-width: 1px;
}
@ -1544,6 +1572,10 @@ html {
border-top-width: 2px;
}
.border-dashed {
border-style: dashed;
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
@ -1670,8 +1702,12 @@ html {
padding: 0.375rem;
}
.p-3 {
padding: 0.75rem;
.p-8 {
padding: 2rem;
}
.p-0 {
padding: 0px;
}
.px-4 {
@ -1714,6 +1750,11 @@ html {
padding-bottom: 1rem;
}
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
@ -1734,11 +1775,6 @@ html {
padding-bottom: 3rem;
}
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
@ -1777,6 +1813,10 @@ html {
padding-right: 2.25rem;
}
.pr-2 {
padding-right: 0.5rem;
}
.pr-12 {
padding-right: 3rem;
}
@ -1801,10 +1841,6 @@ html {
padding-top: 2rem;
}
.pr-2 {
padding-right: 0.5rem;
}
.pt-4 {
padding-top: 1rem;
}
@ -1829,6 +1865,14 @@ html {
padding-left: 0.375rem;
}
.pb-6 {
padding-bottom: 1.5rem;
}
.pb-2 {
padding-bottom: 0.5rem;
}
.text-left {
text-align: left;
}
@ -2084,6 +2128,11 @@ html {
--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity));
}
.ring-gray-300 {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity));
}
.ring-opacity-5 {
--tw-ring-opacity: 0.05;
}
@ -2170,6 +2219,11 @@ html {
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.focus-within\:outline-none:focus-within {
outline: 2px solid transparent;
outline-offset: 2px;
}
.focus-within\:ring-2:focus-within {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@ -2181,6 +2235,11 @@ html {
--tw-ring-color: rgb(36 56 135 / var(--tw-ring-opacity));
}
.focus-within\:ring-indigo-500:focus-within {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity));
}
.focus-within\:ring-offset-2:focus-within {
--tw-ring-offset-width: 2px;
}
@ -2258,6 +2317,11 @@ html {
color: inherit;
}
.hover\:text-indigo-500:hover {
--tw-text-opacity: 1;
color: rgb(99 102 241 / var(--tw-text-opacity));
}
.focus\:border-transparent:focus {
border-color: transparent;
}
@ -2277,6 +2341,11 @@ html {
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.focus\:border-indigo-600:focus {
--tw-border-opacity: 1;
border-color: rgb(79 70 229 / var(--tw-border-opacity));
}
.focus\:placeholder-gray-400:focus::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
@ -2352,11 +2421,36 @@ html {
--tw-ring-offset-color: #f3f4f6;
}
.disabled\:cursor-not-allowed:disabled {
cursor: not-allowed;
}
.disabled\:opacity-30:disabled {
opacity: 0.3;
}
.group:hover .group-hover\:text-gray-500 {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
@media (prefers-color-scheme: dark) {
.dark\:border-gray-600 {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity));
}
.dark\:bg-gray-700 {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.dark\:bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
}
@media (min-width: 640px) {
.sm\:col-span-3 {
grid-column: span 3 / span 3;
@ -2404,6 +2498,10 @@ html {
display: block;
}
.sm\:inline {
display: inline;
}
.sm\:flex {
display: flex;
}
@ -2532,16 +2630,16 @@ html {
padding-right: 1.5rem;
}
.sm\:py-5 {
padding-top: 1.25rem;
padding-bottom: 1.25rem;
}
.sm\:px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
.sm\:py-5 {
padding-top: 1.25rem;
padding-bottom: 1.25rem;
}
.sm\:pl-6 {
padding-left: 1.5rem;
}
@ -2578,6 +2676,19 @@ html {
grid-column: span 2 / span 2;
}
.md\:col-span-3 {
grid-column: span 3 / span 3;
}
.md\:col-span-6 {
grid-column: span 6 / span 6;
}
.md\:mx-0 {
margin-left: 0px;
margin-right: 0px;
}
.md\:ml-0 {
margin-left: 0px;
}
@ -2711,6 +2822,14 @@ html {
margin-right: -2rem;
}
.lg\:table-cell {
display: table-cell;
}
.lg\:hidden {
display: none;
}
.lg\:max-w-7xl {
max-width: 80rem;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 374.2 33.2"><defs><style>.cls-1{fill:#ff1300;}.cls-2{fill:#fff;}</style></defs><g id="Calque_2" data-name="Calque 2"><g id="Calque_1-2" data-name="Calque 1"><path class="cls-1" d="M300.9.6a4.7,4.7,0,0,1,4.9,4.9V32.6h-8.6a4.7,4.7,0,0,1-4.9-4.9V14.1h-.9c-.6,0-1,.6-1,1.5v17H282a4.7,4.7,0,0,1-4.9-4.9V14.1h-1a1.4,1.4,0,0,0-.9,1.5v17H263a1.2,1.2,0,0,1-1.2-1.2V5.5A4.7,4.7,0,0,1,266.7.6h17.9l-.7,5a.9.9,0,0,0,.7,1.1.9.9,0,0,0,1-.8l.8-4.6a.9.9,0,0,1,.9-.7Z"/><path class="cls-1" d="M341.1,16.4c0,9.1-1.9,16.8-16.5,16.8S307.8,25,307.8,16.4,310.6,0,324.6,0,341.1,7.8,341.1,16.4Z"/><path class="cls-1" d="M344.3,32.6a1.2,1.2,0,0,1-1.2-1.2V1.8A1.2,1.2,0,0,1,344.3.6h18.8c6.9,0,10.4,2.9,10.4,8.1s-1.6,6.9-4.5,7.5h0c2.9.6,5.2,3.2,5.2,7.7s-3.5,8.6-10.4,8.6Z"/><path class="cls-2" d="M16.2,11.6c0-.6-.4-.9-1-.9h-.8V31.4a1.2,1.2,0,0,1-1.2,1.2H1.2A1.2,1.2,0,0,1,0,31.4V1.8A1.2,1.2,0,0,1,1.2.6H16.8C25.7.6,30.9,4.2,31,10.7s-4.1,9.9-9.4,10H16.1Z"/><path class="cls-2" d="M44.9,23.1H43.2L40,31.9a1.1,1.1,0,0,1-1.2.7H26.9a1.3,1.3,0,0,1-1.3-1.9L37,3.4A4.5,4.5,0,0,1,41.3.6h5.8a4.1,4.1,0,0,1,4,2.6L62.6,30.8c.5,1-.2,1.8-1.3,1.8H41.7Z"/><path class="cls-2" d="M66.1,32.6a1.2,1.2,0,0,1-1.2-1.2V1.8A1.2,1.2,0,0,1,66.1.6H83.2C92.7.6,95.5,7,95.6,12.3c0,3.3-.8,6.9-3.4,8.3l3.4,12H84.4c-2.3,0-5.2-.7-5.9-2.7L71.9,10.7a1,1,0,0,0-1.2-.7,1,1,0,0,0-.5,1.3l7.2,21.3Z"/><path class="cls-2" d="M129.6,17.1c.8,0,1.3.5,1.2,1.2-.2,8.3-2.9,14.9-16.4,14.9-15.8.1-16.8-8.2-16.8-16.8S100.5,0,114.4,0c15.7,0,16.5,8,16.5,15.4H115.3V11.3c0-.8-.4-1.2-1-1.2s-.9.4-.9,1.2V21.9c0,.9.4,1.2.9,1.2s1-.3,1-1.2V17.1Z"/><path class="cls-2" d="M166.2,16.4c0,9.1-2,16.8-16.6,16.8S132.8,25,132.8,16.4,135.7,0,149.6,0,166.2,7.8,166.2,16.4Z"/><path class="cls-2" d="M200.5,1.8V12.2c0,12.7-1.4,21-16.3,21s-16-8.4-16-20.9V1.8A1.2,1.2,0,0,1,169.4.6h13a.9.9,0,0,1,.9.9V21.8c0,.9.4,1.2,1,1.2s.9-.3.9-1.2V1.5a.9.9,0,0,1,.9-.9h13.1A1.2,1.2,0,0,1,200.5,1.8Z"/><path class="cls-2" d="M203.9,32.6a1.2,1.2,0,0,1-1.3-1.2V1.8A1.2,1.2,0,0,1,203.8.6h17.1c9.5,0,12.3,6.4,12.4,11.7,0,3.3-.8,6.9-3.4,8.3l3.4,12H222.1c-2.3,0-5.2-.7-5.9-2.7l-6.6-19.2a1,1,0,0,0-1.2-.7,1,1,0,0,0-.5,1.3l7.2,21.3Z"/><path class="cls-2" d="M235.8,29.2v-10a1,1,0,0,1,1.5-1l11.1,5.1a1,1,0,0,0,1.2-.4.9.9,0,0,0-.6-1.2l-9.3-4.4c-4.5-2.2-4.4-5.5-4.4-8.2,0-7.6,7.4-9.1,14.6-9.1,5.4,0,12.1,1.8,14.3,4.4v9.2a1,1,0,0,1-1.5,1L252.3,9.8a1,1,0,0,0-1.3.4q-.3.8.6,1.2l8.5,4.1c5.3,2.6,4.7,4.9,4.7,9.2s-6.3,8.5-14.9,8.5C244.7,33.2,238,31.6,235.8,29.2Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

104
utils/cache/cache.go vendored
View File

@ -1,104 +0,0 @@
package cache
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/spf13/viper"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/namespace"
)
type CacheHandler struct {
*clientv3.Client
}
func NewCacheHandler(cfg *viper.Viper) (*CacheHandler, error) {
var (
endpoints = cfg.GetStringSlice("cache.storage.etcd.endpoints")
prefix = cfg.GetString("cache.storage.etcd.prefix")
)
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, err
}
cli.KV = namespace.NewKV(cli.KV, prefix)
cli.Watcher = namespace.NewWatcher(cli.Watcher, prefix)
cli.Lease = namespace.NewLease(cli.Lease, prefix)
return &CacheHandler{
Client: cli,
}, nil
}
func (s *CacheHandler) Put(k string, v any) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
// _, err = s.Client.KV.Put(context.TODO(), k, data.String())
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = s.Client.KV.Put(ctx, k, string(data))
cancel()
if err != nil {
return err
}
return nil
}
func (s *CacheHandler) PutWithTTL(k string, v any, duration time.Duration) error {
lease, err := s.Client.Lease.Grant(context.TODO(), int64(duration.Seconds()))
if err != nil {
return err
}
data, err := json.Marshal(v)
if err != nil {
return err
}
// _, err = s.Client.KV.Put(context.TODO(), k, data.String(), clientv3.WithLease(lease.ID))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = s.Client.KV.Put(ctx, k, string(data), clientv3.WithLease(lease.ID))
cancel()
if err != nil {
return err
}
return nil
}
func (s *CacheHandler) Get(k string) (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
resp, err := s.Client.KV.Get(ctx, k)
cancel()
if err != nil {
return nil, err
}
for _, v := range resp.Kvs {
var data any
err := json.Unmarshal([]byte(v.Value), &data)
if err != nil {
return nil, err
}
// We return directly as we want to last revision of value
return data, nil
}
return nil, errors.New(fmt.Sprintf("no value %v", k))
}
func (s *CacheHandler) Delete(k string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err := s.Client.KV.Delete(ctx, k)
cancel()
if err != nil {
return err
}
return nil
}

View File

@ -19,6 +19,7 @@ func (p *IdentificationProvider) GroupsMiddleware(next http.Handler) http.Handle
o, ok := session.Values["organization"]
if !ok || o == nil {
fmt.Println("no organization")
http.Redirect(w, r, "/auth/groups/", http.StatusFound)
return
}

View File

@ -9,6 +9,7 @@ import (
"net/http"
"git.coopgo.io/coopgo-apps/parcoursmob/services"
"git.coopgo.io/coopgo-apps/parcoursmob/utils/storage"
"github.com/coreos/go-oidc"
"github.com/gorilla/sessions"
"github.com/spf13/viper"
@ -28,7 +29,7 @@ type IdentificationProvider struct {
Services *services.ServicesHandler
}
func NewIdentificationProvider(cfg *viper.Viper, services *services.ServicesHandler) (*IdentificationProvider, error) {
func NewIdentificationProvider(cfg *viper.Viper, services *services.ServicesHandler, kv storage.KVHandler) (*IdentificationProvider, error) {
var (
providerURL = cfg.GetString("identification.oidc.provider")
clientID = cfg.GetString("identification.oidc.client_id")
@ -54,7 +55,7 @@ func NewIdentificationProvider(cfg *viper.Viper, services *services.ServicesHand
Scopes: []string{oidc.ScopeOpenID, "groups", "first_name", "last_name", "display_name", "email"},
}
var store = sessions.NewCookieStore([]byte(sessionsSecret))
store := storage.NewSessionStore(kv, []byte(sessionsSecret))
verifier := provider.Verifier(&oidc.Config{ClientID: oauth2Config.ClientID})
return &IdentificationProvider{

View File

@ -0,0 +1,13 @@
package sorting
import (
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
)
type BeneficiariesByName []mobilityaccountsstorage.Account
func (e BeneficiariesByName) Len() int { return len(e) }
func (e BeneficiariesByName) Less(i, j int) bool {
return e[i].Data["first_name"].(string) < e[j].Data["first_name"].(string)
}
func (e BeneficiariesByName) Swap(i, j int) { e[i], e[j] = e[j], e[i] }

11
utils/sorting/events.go Normal file
View File

@ -0,0 +1,11 @@
package sorting
import (
agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
)
type EventsByStartdate []agendastorage.Event
func (e EventsByStartdate) Len() int { return len(e) }
func (e EventsByStartdate) Less(i, j int) bool { return e[i].Startdate.Before(e[j].Startdate) }
func (e EventsByStartdate) Swap(i, j int) { e[i], e[j] = e[j], e[i] }

23
utils/sorting/fleets.go Normal file
View File

@ -0,0 +1,23 @@
package sorting
import (
"strings"
fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
)
type VehiclesByLicencePlate []fleetsstorage.Vehicle
func (a VehiclesByLicencePlate) Len() int { return len(a) }
func (a VehiclesByLicencePlate) Less(i, j int) bool {
return strings.Compare(a[i].Data["licence_plate"].(string), a[j].Data["licence_plate"].(string)) < 0
}
func (a VehiclesByLicencePlate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type BookingsByStartdate []fleetsstorage.Booking
func (a BookingsByStartdate) Len() int { return len(a) }
func (a BookingsByStartdate) Less(i, j int) bool {
return a[i].Startdate.Before(a[j].Startdate)
}
func (a BookingsByStartdate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

15
utils/sorting/groups.go Normal file
View File

@ -0,0 +1,15 @@
package sorting
import (
"strings"
groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
)
type GroupsByName []groupstorage.Group
func (a GroupsByName) Len() int { return len(a) }
func (a GroupsByName) Less(i, j int) bool {
return strings.Compare(a[i].Data["name"].(string), a[j].Data["name"].(string)) < 0
}
func (a GroupsByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

1
utils/sorting/sorting.go Normal file
View File

@ -0,0 +1 @@
package sorting

11
utils/storage/cache.go Normal file
View File

@ -0,0 +1,11 @@
package storage
import (
"github.com/spf13/viper"
)
type CacheHandler KVHandler
func NewCacheHandler(cfg *viper.Viper) (CacheHandler, error) {
return NewKVHandler(cfg)
}

149
utils/storage/etcd.go Normal file
View File

@ -0,0 +1,149 @@
package storage
import (
"bytes"
"context"
"encoding/gob"
"encoding/json"
"fmt"
"time"
"github.com/spf13/viper"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/namespace"
)
type EtcdSerializer interface {
Deserialize(d []byte, m *any) error
Serialize(m any) ([]byte, error)
}
type JSONEtcdSerializer struct{}
// Serialize to JSON. Will err if there are unmarshalable key values
func (s JSONEtcdSerializer) Serialize(m any) ([]byte, error) {
return json.Marshal(m)
}
// Deserialize back to map[string]interface{}
func (s JSONEtcdSerializer) Deserialize(d []byte, m *any) (err error) {
err = json.Unmarshal(d, &m)
if err != nil {
fmt.Printf("JSONSerializer.deserialize() Error: %v", err)
return err
}
return
}
// GobEtcdSerializer uses gob package to encode the session map
type GobEtcdSerializer struct{}
// Serialize using gob
func (s GobEtcdSerializer) Serialize(m any) ([]byte, error) {
buf := new(bytes.Buffer)
enc := gob.NewEncoder(buf)
err := enc.Encode(m)
if err == nil {
return buf.Bytes(), nil
}
return nil, err
}
// Deserialize back to map[interface{}]interface{}
func (s GobEtcdSerializer) Deserialize(d []byte, m any) error {
dec := gob.NewDecoder(bytes.NewBuffer(d))
return dec.Decode(&m)
}
type EtcdHandler struct {
*clientv3.Client
serializer EtcdSerializer
}
func NewEtcdHandler(cfg *viper.Viper) (*EtcdHandler, error) {
var (
endpoints = cfg.GetStringSlice("storage.kv.etcd.endpoints")
prefix = cfg.GetString("storage.kv.etcd.prefix")
)
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, err
}
cli.KV = namespace.NewKV(cli.KV, prefix)
cli.Watcher = namespace.NewWatcher(cli.Watcher, prefix)
cli.Lease = namespace.NewLease(cli.Lease, prefix)
return &EtcdHandler{
Client: cli,
serializer: JSONEtcdSerializer{},
}, nil
}
func (s *EtcdHandler) Put(k string, v any) error {
data, err := s.serializer.Serialize(v)
if err != nil {
return err
}
// _, err = s.Client.KV.Put(context.TODO(), k, data.String())
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = s.Client.KV.Put(ctx, k, string(data))
cancel()
if err != nil {
return err
}
return nil
}
func (s *EtcdHandler) PutWithTTL(k string, v any, duration time.Duration) error {
lease, err := s.Client.Lease.Grant(context.TODO(), int64(duration.Seconds()))
if err != nil {
return err
}
data, err := s.serializer.Serialize(v)
if err != nil {
return err
}
// _, err = s.Client.KV.Put(context.TODO(), k, data.String(), clientv3.WithLease(lease.ID))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err = s.Client.KV.Put(ctx, k, string(data), clientv3.WithLease(lease.ID))
cancel()
if err != nil {
return err
}
return nil
}
func (s *EtcdHandler) Get(k string) (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
resp, err := s.Client.KV.Get(ctx, k)
cancel()
if err != nil {
return nil, err
}
for _, v := range resp.Kvs {
var data any
err := s.serializer.Deserialize([]byte(v.Value), &data)
if err != nil {
return nil, err
}
// We return directly as we want to last revision of value
return data, nil
}
return nil, fmt.Errorf("no value %v", k)
}
func (s *EtcdHandler) Delete(k string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err := s.Client.KV.Delete(ctx, k)
cancel()
if err != nil {
return err
}
return nil
}

30
utils/storage/files.go Normal file
View File

@ -0,0 +1,30 @@
package storage
import (
"io"
"time"
"github.com/spf13/viper"
)
const (
PREFIX_BENEFICIARIES = "beneficiaries"
)
type FileInfo struct {
Key string
FileName string
LastModified time.Time
ContentType string
Metadata map[string]string
}
type FileStorage interface {
Put(reader io.Reader, prefix string, filename string, size int64, metadata map[string]string) error
List(prefix string) []FileInfo
Get(prefix string, file string) (io.Reader, *FileInfo, error)
}
func NewFileStorage(cfg *viper.Viper) (FileStorage, error) {
return NewMinioStorageHandler(cfg)
}

18
utils/storage/kv.go Normal file
View File

@ -0,0 +1,18 @@
package storage
import (
"time"
"github.com/spf13/viper"
)
type KVHandler interface {
Put(k string, v any) error
PutWithTTL(k string, v any, duration time.Duration) error
Get(k string) (any, error)
Delete(k string) error
}
func NewKVHandler(cfg *viper.Viper) (KVHandler, error) {
return NewEtcdHandler(cfg)
}

104
utils/storage/minio.go Normal file
View File

@ -0,0 +1,104 @@
package storage
import (
"context"
"fmt"
"io"
"strings"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/spf13/viper"
)
type MinioStorageHandler struct {
*minio.Client
BucketName string
}
func NewMinioStorageHandler(cfg *viper.Viper) (*MinioStorageHandler, error) {
minioClient, err := minio.New(cfg.GetString("storage.files.minio.endpoint"), &minio.Options{
Creds: credentials.NewStaticV4(cfg.GetString("storage.files.minio.access_key"), cfg.GetString("storage.files.minio.secret_key"), ""),
Secure: cfg.GetBool("storage.files.minio.use_ssl"),
})
if err != nil {
fmt.Println(err)
return nil, err
}
return &MinioStorageHandler{
Client: minioClient,
BucketName: cfg.GetString("storage.files.minio.bucket_name"),
}, nil
}
func (s *MinioStorageHandler) Put(reader io.Reader, prefix string, filename string, size int64, metadata map[string]string) error {
s.Client.PutObject(context.TODO(), s.BucketName, prefix+"/"+filename, reader, size, minio.PutObjectOptions{
UserMetadata: metadata,
})
return nil
}
func (s *MinioStorageHandler) List(prefix string) []FileInfo {
res := []FileInfo{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
objectCh := s.Client.ListObjects(ctx, s.BucketName, minio.ListObjectsOptions{
Prefix: prefix,
Recursive: true,
})
for object := range objectCh {
if object.Err != nil {
fmt.Println("Error : ", object.Err)
continue
}
objinfo, err := s.Client.StatObject(context.Background(), s.BucketName, object.Key, minio.StatObjectOptions{})
if err != nil {
fmt.Println(err)
continue
}
path := strings.Split(object.Key, "/")
fileinfo := FileInfo{
Key: object.Key,
FileName: path[len(path)-1],
LastModified: object.LastModified,
ContentType: object.ContentType,
Metadata: objinfo.UserMetadata,
}
res = append(res, fileinfo)
}
return res
}
func (s *MinioStorageHandler) Get(prefix string, file string) (io.Reader, *FileInfo, error) {
object, err := s.Client.GetObject(context.Background(), s.BucketName, prefix+"/"+file, minio.GetObjectOptions{})
if err != nil {
fmt.Println(err)
return nil, nil, err
}
objinfo, err := s.Client.StatObject(context.Background(), s.BucketName, prefix+"/"+file, minio.StatObjectOptions{})
if err != nil {
fmt.Println(err)
return nil, nil, err
}
path := strings.Split(objinfo.Key, "/")
fileinfo := &FileInfo{
Key: objinfo.Key,
FileName: path[len(path)-1],
LastModified: objinfo.LastModified,
ContentType: objinfo.ContentType,
Metadata: objinfo.UserMetadata,
}
return object, fileinfo, nil
}

144
utils/storage/sessions.go Normal file
View File

@ -0,0 +1,144 @@
package storage
import (
"context"
"encoding/base32"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
)
// Amount of time for cookies/kv keys to expire.
var sessionExpire = 86400 * 30
type SessionStore struct {
KV KVHandler
Codecs []securecookie.Codec
options *sessions.Options // default configuration
DefaultMaxAge int // default TiKV TTL for a MaxAge == 0 session
//maxLength int
keyPrefix string
//serializer SessionSerializer
}
func NewSessionStore(client KVHandler, keyPairs ...[]byte) *SessionStore {
es := &SessionStore{
KV: client,
Codecs: securecookie.CodecsFromPairs(keyPairs...),
options: &sessions.Options{
Path: "/",
MaxAge: sessionExpire,
},
DefaultMaxAge: sessionExpire, // 20 minutes seems like a reasonable default
//maxLength: 4096,
keyPrefix: "session/",
}
return es
}
func (s *SessionStore) Get(r *http.Request, name string) (*sessions.Session, error) {
// session := sessions.NewSession(s, name)
// ok, err := s.load(r.Context(), session)
// if !(err == nil && ok) {
// if err == nil {
// err = errors.New("key does not exist")
// }
// }
// return session, err
return sessions.GetRegistry(r).Get(s, name)
}
func (s *SessionStore) New(r *http.Request, name string) (*sessions.Session, error) {
session := sessions.NewSession(s, name)
options := *s.options
session.Options = &options
session.IsNew = true
if c, errCookie := r.Cookie(name); errCookie == nil {
err := securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...)
if err != nil {
return session, err
}
ok, err := s.load(r.Context(), session)
session.IsNew = !(err == nil && ok) // not new if no error and data available
}
return session, nil
}
// Save adds a single session to the response.
func (s *SessionStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
// Marked for deletion.
if session.Options.MaxAge <= 0 {
if err := s.delete(r.Context(), session); err != nil {
return err
}
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
} else {
// Build an alphanumeric key for the kv store.
if session.ID == "" {
session.ID = strings.TrimRight(base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), "=")
}
if err := s.save(r.Context(), session); err != nil {
return err
}
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, s.Codecs...)
if err != nil {
return err
}
http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
}
return nil
}
// save stores the session in kv.
func (s *SessionStore) save(ctx context.Context, session *sessions.Session) error {
m := make(map[string]interface{}, len(session.Values))
for k, v := range session.Values {
ks, ok := k.(string)
if !ok {
err := fmt.Errorf("non-string key value, cannot serialize session: %v", k)
return err
}
m[ks] = v
}
age := session.Options.MaxAge
if age == 0 {
age = s.DefaultMaxAge
}
return s.KV.PutWithTTL(s.keyPrefix+session.ID, m, time.Duration(age)*time.Second)
}
// load reads the session from kv store.
// returns true if there is a sessoin data in DB
func (s *SessionStore) load(ctx context.Context, session *sessions.Session) (bool, error) {
data, err := s.KV.Get(s.keyPrefix + session.ID)
if err != nil {
return false, err
}
if data == nil && err == nil {
return false, errors.New("key does not exist")
}
for k, v := range data.(map[string]any) {
session.Values[k] = v
}
return true, nil
}
// delete removes keys from tikv if MaxAge<0
func (s *SessionStore) delete(ctx context.Context, session *sessions.Session) error {
if err := s.KV.Delete(s.keyPrefix + session.ID); err != nil {
return err
}
return nil
}