Compare commits

...

86 commits
v5.6.1 ... main

Author SHA1 Message Date
68074dcd85 Merge pull request 'docs: rapporter ancien README.md' (#69) from vlbeaudoin/docs/readme into main
Reviewed-on: #69
2024-09-19 14:41:55 -04:00
1ad0d61477 docs: rapporter ancien README.md 2024-09-19 14:41:24 -04:00
e96033eb80 Merge pull request 'major: séparer commande de librairie importable' (#67) from vlbeaudoin/major/v9 into main
Reviewed-on: #67
2024-09-18 19:14:04 -04:00
b419a5b260 major: séparer commande de librairie importable
Bump major version à 9

package main déplacé vers cmd/bottin/ pour garder `go install` qui nomme
l'exécutable `bottin`, sans empêcher d'importer le code à l'extérieur du
projet avec pkg/bottin/.

Déplacer fichiers SQL vers queries/

Déplacer fichiers html vers templates/

Ajouter scripts/ avec génération et injection de certificats x509
(https) et les ajouter au Makefile

Ajouter début d'exemple de manifests dans deployments/kubernetes/
2024-09-18 19:06:33 -04:00
a17d6bf06c Merge pull request 'fix(Dockerfile): utiliser version de alpine qui existe actually' (#66) from vlbeaudoin/fix/dockerfile-alpine-version into main
Reviewed-on: #66
2024-09-03 17:07:02 -04:00
0640395fd2 fix(Dockerfile): utiliser version de alpine qui existe actually 2024-09-03 17:06:10 -04:00
d29ef1b5ef Merge pull request 'chores(Dockerfile): bump alpine -> 3.20.3' (#65) from vlbeaudoin/chores/bump-alpine into main
Reviewed-on: #65
2024-09-03 17:03:09 -04:00
545b38c0db Merge pull request 'chores(Dockerfile): bump golang -> 1.23.0' (#64) from vlbeaudoin/chores/bump-go into main
Reviewed-on: #64
2024-09-03 17:03:02 -04:00
882553521a chores(Dockerfile): bump alpine -> 3.20.3 2024-09-03 17:02:20 -04:00
c9a45f8db8 chores(Dockerfile): bump golang -> 1.23.0 2024-09-03 17:01:34 -04:00
f874c449a7 Merge pull request 'feature(compose): directly inject .env file à containers api et ui' (#63) from vlbeaudoin/feature/inject-env-file into main
Reviewed-on: #63
2024-09-03 16:58:53 -04:00
bdff81c6b2 feature(compose): directly inject .env file à containers api et ui 2024-09-03 16:58:25 -04:00
c0b8ceafa9 Merge pull request 'feature(cmd): implémenter UI API TLS skip verify' (#62) from vlbeaudoin/feature/ui-allow-selfsigned-api-tls into main
Reviewed-on: #62
2024-09-03 16:44:18 -04:00
9072f7114a feature(cmd): implémenter UI API TLS skip verify 2024-09-03 16:43:22 -04:00
2b6c631d64 fix(compose): adjust .env inject 2024-09-03 16:42:25 -04:00
7ddf89a859 feature(config): add server.ui.api.tls.skipverify 2024-09-03 16:42:14 -04:00
1a847209f4 Merge pull request 'feature: ajouter cfg.Server.UI.Host et implémenter UI TLS' (#61) from vlbeaudoin/feature/ui-tls into main
Reviewed-on: #61
2024-07-23 11:48:40 -04:00
f5aa25a12a feature: ajouter cfg.Server.UI.Host et implémenter UI TLS 2024-07-23 11:46:37 -04:00
61c4ef80f5 Merge pull request 'fix: considérer cfg.Server.API.Host' (#60) from vlbeaudoin/fix/api-check-host into main
Reviewed-on: #60
2024-07-23 11:45:52 -04:00
8a9decfe6c fix: considérer cfg.Server.API.Host
Valeur n'avait aucun effet précédemment, permet maintenant de choisir
sur quel hôte le serveur API est rejoignable
2024-07-23 11:44:41 -04:00
31bcdbac20 Merge pull request 'fix: vérifier existence de certfile et keyfile pour API' (#59) from vlbeaudoin/fix/check-api-cert-and-key into main
Reviewed-on: #59
2024-07-23 11:41:47 -04:00
03c9ad5f3c fix: vérifier existence de certfile et keyfile pour API
Au lieu de print leur valeur à l'écran
2024-07-23 11:40:40 -04:00
537f1a8a1a Merge pull request 'rework: config and cmd' (#58) from vlbeaudoin/rework/config into main
Reviewed-on: #58
2024-07-15 16:55:02 -04:00
e14ff3d04e Merge pull request 'fix: implémenter correctement tls certfile et keyfile' (#55) from vlbeaudoin/fix/tls into main
Reviewed-on: #55
2024-09-06 20:38:02 -05:00
6579ea45f9 Merge pull request 'Permettre d'exposer le serveur API par https' (#54) from vlbeaudoin/feature/tls-api into main
Reviewed-on: #54
2024-09-03 13:29:11 -05:00
98090c96ac Merge pull request 'Version 7' (#53) from rewrite/v7 into main
Reviewed-on: #53
2024-09-03 10:17:25 -05:00
eb1982898c rework: config and cmd
Renamed `web` command to `server ui` (web is still an alias to ui)

Completely changed the config options and flags

Usage of PersistentFlags now allow clearer `--help`

BREAKING: cmd modified
BREAKING: config overhauled
BREAKING: Bump API to v8
2024-07-15 16:52:04 -04:00
8c074dd443 fix: implémenter correctement tls certfile et keyfile
test: ne pas vérifier le certificat avant de l'accepter
2024-07-07 03:58:15 -04:00
a9f1682634 fix(test): ajuster TLS client voki selon config 2024-07-03 20:53:17 -04:00
4ce3d9f60b feature(api): permettre d'exposer le serveur API par https
Requiert `cfg.API.TLS.Enabled = true` et des fichiers valides pour
`cfg.API.TLS.{CertificateFile,PrivateKeyFile}`
2024-07-03 20:51:57 -04:00
150782c42f feature(config): ajouter options TLS 2024-07-03 20:51:43 -04:00
d80c7675f9 fix(routes): unused param cfg 2024-07-03 17:37:29 -04:00
1f2ba0576a feature: permettre insert par csv
Ajouter parameter cfg à addRoutes()

Fix empty et default limit sur get requests (set default limit à 1000 hardcoded,
todo move to config)
2024-07-03 17:34:18 -04:00
14eb6c5d02 ajouter examples/example.csv 2024-07-03 17:33:56 -04:00
d0de811547 chores: update dependencies 2024-06-21 18:46:45 -04:00
64ddfa96d6 fix: franciser erreur de membre non trouvé·e 2024-06-20 20:20:30 -04:00
8af11615dd adjust: ajouter emojis à certaines web responses 2024-06-20 20:16:33 -04:00
6cc90b1a29 feature(web): ajouter route /membre/
permet la recherche de membre
2024-06-20 19:55:12 -04:00
0321b1b2a0 fix(web): correctement render erreur d'accès au serveur API 2024-06-20 19:54:41 -04:00
244276905b feature(cmd): implémenter webCmd de base
manque encore le processus de scan mais sinon c'est presque fini
2024-06-20 19:36:38 -04:00
7484bafc84 fix(web): neutraliser texte avec middle dot (·) 2024-06-20 19:35:07 -04:00
8cb2014f3b fix(template): expect voki.MessageResponse in input object 2024-06-20 19:34:27 -04:00
929704c6ff fix(config): ajouter préfixe web[.-] aux options config web 2024-06-20 19:32:26 -04:00
e4ff1013d0 feature: ajouter et tester GetMembre[s]ForDisplay
Priorisent le prefered_name ("nom d'usage") et devraient être utilisés
aux endroits où l'affichage est important.
2024-06-20 18:51:38 -04:00
e6103c6e6e feature(api): add and test UpdateMembrePreferedName 2024-06-19 00:28:26 -04:00
78aafe0ce9 feature(api): add and test ProgrammesGET 2024-06-19 00:04:19 -04:00
26b3134861 feature(request): ajouter MembrePreferedNamePUT et ProgrammesGET 2024-06-18 23:55:55 -04:00
4d338f2b03 feature: ajouter ProgrammesGETResponse et data 2024-06-18 22:51:32 -04:00
f6ffa03379 feature: ajouter MembrePreferedNamePUTResponse 2024-06-18 22:51:20 -04:00
f8b5c72003 feature: add and test GetMembres 2024-06-18 21:21:30 -04:00
00aebc2ae3 feature: add basic Makefile for integration testing 2024-06-18 19:47:28 -04:00
c7c64674c7 rework: change api prefix to /api/v7/
- add and test GetMembre
- add `IsMembreID(string) bool` function

BREAKING: Rename routes to `/api/v7/...` scheme
2024-06-18 19:44:20 -04:00
e847f693e0 rework: renommer champs dans entities et ajouter MembresPOST
- ajouter et tester InsertMembres
- ajouter sql/views.sql
- ajouter view `membres_for_display` -> concat names ou prefered name
- rendre plusieurs champs NOT NULL dans schema
2024-06-17 17:25:53 -04:00
e1bce94d18 feature: add and test ProgrammesPOST 2024-06-17 14:07:49 -04:00
c5339bd45b fix(Dockerfile): copier fichiers go manquants vers image 2024-06-17 14:06:43 -04:00
be766f593d ajouter API client et tester /api/health 2024-06-11 17:28:20 -04:00
eca5ffa7fb feature(db): Ajouter InsertMembres, InsertProgrammes et GetMembres 2024-06-10 17:25:01 -04:00
1b04237c96 ajouter fichiers manquants à Dockerfile build step 2024-06-07 15:18:22 -04:00
1125104280 chores: go get -u 2024-06-07 14:59:49 -04:00
780d493dc1 split cmd
cmd.go contient maintenant juste les actual commandes.
Les fonctionalités liées à la configuration sont dans config.go, et les
fonctionalités liées au templating est dans template.go.
2024-06-06 18:07:30 -04:00
cdd526a6f3 wip: make apiCmd run and remove db test 2024-06-06 17:59:58 -04:00
0123d9d37c wip: integration between cmd.go and config.go 2024-06-06 17:01:16 -04:00
b67955ab28 wip: merge cmd package into main package 2024-06-06 16:28:14 -04:00
6d98375adb début de réécriture pour v7 2024-06-06 01:40:56 -04:00
369332db26 Merge pull request 'chores(Dockerfile): bump alpine to 3.19' (#52) from chores/bump-alpine-3.19 into main
Reviewed-on: #52
2024-02-15 19:38:42 -05:00
917aab7e01 chores(Dockerfile): bump alpine to 3.19 2024-02-15 19:38:02 -05:00
522b2d7041 Merge pull request 'chores(Dockerfile): bump go version to v1.22.0' (#51) from chores/bump-go-to-1.22 into main
Reviewed-on: #51
2024-02-15 19:37:41 -05:00
4a87daae79 chores(Dockerfile): bump go version to v1.22.0 2024-02-15 19:37:06 -05:00
1b5e0913a6 Merge pull request 'chores: bump voki to v2.0.3' (#50) from chores/bump-voki-to-v2 into main
Reviewed-on: #50
2024-02-15 19:33:22 -05:00
9367f0f4c0 chores: bump voki to v2.0.3 2024-02-15 19:32:50 -05:00
3f0bf238e0 Merge pull request 'chores: bump go.mod dependencies' (#49) from chores/go-get-update into main
Reviewed-on: #49
2024-02-15 19:27:33 -05:00
4c8e822324 chores: bump go.mod dependencies
Execute `go get -u`
2024-02-15 19:26:44 -05:00
4544940556 Merge pull request 'Escalader getmembreresponse message as error if no returned membre' (#48) from fix/escalate-getmembreresponse-message-as-error into main
Reviewed-on: #48
2024-02-14 16:38:38 -05:00
81d775e5a6 fix: escalate getmembreresponse message as error if no returned membre 2024-02-14 16:36:44 -05:00
d48bf545d7 Merge pull request 'Defer certains appels à tx.Rollback' (#47) from fix/defer-rollback into main
Reviewed-on: #47
2024-02-14 14:13:53 -05:00
d5399903e4 fix: defer certains appels à tx.Rollback
Pour `data.InsertMembres` et `data.InsertProgrammes`
2024-02-14 14:13:01 -05:00
585c626e1c Merge pull request 'Permettre de configurer api et web par .env' (#46) from feature/config-depuis-env into main
Reviewed-on: #46
2024-02-14 14:08:49 -05:00
f7437d1719 feat: Permettre de configurer api et web par .env
L'ajout à viper de replacer et préfixe `BOTTIN` permet de déployer et
configurer l'application avec seulement docker-compose, en évitant
d'avoir à nécessairement uploader un fichier de config.

Ajoute aussi des explications dans `README.md` sur changements de
procédure
2024-02-14 14:05:04 -05:00
263d312b36 Merge pull request 'license: remplacer license pour GNU GPLv2' (#45) from license/gplv2 into main
Reviewed-on: #45
2024-01-05 15:19:07 -05:00
50155ed9cb license: remplacer license pour GNU GPLv2 2024-01-05 15:18:21 -05:00
f448d409ee Merge pull request 'Identifier formats json et csv permis lors d'insertion' (#44) from fix/identifier-support-formats-insertion into main
Reviewed-on: #44
2024-01-05 14:51:02 -05:00
b8f05cb266 fix: Identifier formats json et csv permis lors d'insertion
Clarifier le message d'erreur d'insertion `Invalid Content-Type` pour inclure les
formats permis
2024-01-05 14:49:44 -05:00
6ee96c3b97 Merge pull request 'Bump API et go mod à v6' (#43) from chores/bump-api-v6 into main
Reviewed-on: #43
2024-01-05 14:48:13 -05:00
a8dcdd0388 chores!: bump API et go mod à v6
Tag v6.0.0 est sorti mais n'était pas réflété dans le code.

BREAKING: API est maintenant exposé sur `/v6` et non `/v5`
2024-01-05 14:38:48 -05:00
fe50cb7335 Merge pull request 'Bump postgres to 16.1' (#41) from chores/bump-postgres-to-16 into main
Reviewed-on: #41
2023-12-28 14:19:30 -05:00
0bbf463674 chores!: bump postgres to 16.1
Existing databases will prevent this from booting. If you want to stay on
postgres 14, modify `docker-compose.yaml`'s `services.db.image`.

BREAKING: update postgres image major version from 14 to 16
2023-12-28 14:18:39 -05:00
48 changed files with 2852 additions and 1604 deletions

2
.gitignore vendored
View file

@ -22,6 +22,8 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
# cert files
*.pem
# env # env
.env .env

View file

@ -1,23 +1,21 @@
FROM golang:1.21.1 as build FROM golang:1.23.0 as build
LABEL author="vlbeaudoin" LABEL author="vlbeaudoin"
WORKDIR /go/src/app WORKDIR /go/src/app
COPY go.mod go.sum main.go ./ COPY go.mod go.sum LICENSE ./
ADD cmd/ cmd/ ADD cmd/ cmd/
ADD data/ data/ ADD pkg/ pkg/
ADD handlers/ handlers/ ADD queries/ queries/
ADD models/ models/ ADD templates/ templates/
ADD responses/ responses/
ADD web/ web/
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o bottin . RUN CGO_ENABLED=0 go build -a -o bottin ./cmd/bottin
# Alpine # Alpine
FROM alpine:3.18 FROM alpine:3.20.2
WORKDIR /app WORKDIR /app

119
LICENSE
View file

@ -1,9 +1,118 @@
MIT License GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (c) 2021-2023 AGECEM Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. Preamble
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

28
Makefile Normal file
View file

@ -0,0 +1,28 @@
## This Makefile uses the help target explained in the following blogpost:
##
## https://victoria.dev/blog/how-to-create-a-self-documenting-makefile/
.DEFAULT_GOAL := help
.PHONY: help
help: ## Show this help
@egrep -h '\s##\s' $(MAKEFILE_LIST) | \
sort | \
awk \
'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
.PHONY: test-integration
test-integration: ## run integration tests through API client. Config is read from `~/.bottin.yaml`. WARNING: affects data in the database, do not run on production server
docker-compose down && docker-compose up -d --build && sleep 2 && go test
.PHONY: dev
dev: generate-self-signed-x509 compose-inject-x509 ## deploy development environment on docker-compose
docker-compose up -d
.PHONY: generate-self-signed-x509
generate-self-signed-x509: ## Générer une paire de clés x509 self-signed pour utilisation avec un serveur de développement
./scripts/generate-self-signed-x509.sh
.PHONY: compose-inject-x509
compose-inject-x509: ## Copie la paire de clés x509 du current directory vers les containers orchestrés par docker-compose
./scripts/compose-inject-x509.sh

View file

@ -20,30 +20,39 @@ https://git.agecem.com/agecem/bottin
Remplir .env avec les infos qui seront utilisées pour déployer le container Remplir .env avec les infos qui seront utilisées pour déployer le container
(Remplacer `bottin` par quelque chose de plus sécuritaire) Au minimum, il faut ces 3 entrées:
*Remplacer `bottin` par quelque chose de plus sécuritaire*
```sh ```sh
BOTTIN_POSTGRES_DATABASE=bottin BOTTIN_SERVER_DB_DATABASE=bottin
BOTTIN_POSTGRES_PASSWORD=bottin BOTTIN_SERVER_DB_PASSWORD=bottin
BOTTIN_POSTGRES_USER=bottin BOTTIN_SERVER_DB_USER=bottin
``` ```
*D'autres entrées peuvent être ajoutées, voir `config.go` pour les options*
Déployer avec docker-compose Déployer avec docker-compose
`$ docker-compose up -d` `$ docker-compose up -d`
### Optionnel: configuration par fichiers YAML
*seulement nécessaire si les fichiers `.env` et `docker-compose.yaml` ne contiennent pas toute l'information nécessaire*
Pour modifier la configuration du serveur API Pour modifier la configuration du serveur API
`$ docker-compose exec -it api vi /etc/bottin/api.yaml` `$ docker-compose exec -it api vi /etc/bottin/api.yaml`
*Y remplir au minimum le champs `api.key` (string)* *Y remplir au minimum le champs `server.api.key` (string)*
Pour modifier la configuration du client web Pour modifier la configuration du client web
`$ docker-compose exec -it web vi /etc/bottin/web.yaml` `$ docker-compose exec -it ui vi /etc/bottin/ui.yaml`
*Y remplir au minimum les champs `web.api.key` (string), `web.user` (string) et `web.password` (string)* *Y remplir au minimum les champs `server.ui.api.key` (string), `server.ui.user` (string) et `server.ui.password` (string)*
Redémarrer les containers une fois la configuration modifiée Redémarrer les containers une fois la configuration modifiée
`$ docker-compose down && docker-compose up -d` `$ docker-compose down && docker-compose up -d`
v

View file

@ -1,122 +0,0 @@
package cmd
import (
"crypto/subtle"
"fmt"
"log"
"codeberg.org/vlbeaudoin/serpents"
"git.agecem.com/agecem/bottin/v5/data"
"git.agecem.com/agecem/bottin/v5/handlers"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
apiPort int
apiKey string
)
// apiCmd represents the api command
var apiCmd = &cobra.Command{
Use: "api",
Short: "Démarrer le serveur API",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
apiKey = viper.GetString("api.key")
apiPort = viper.GetInt("api.port")
e := echo.New()
// Middlewares
e.Pre(middleware.AddTrailingSlash())
if apiKey != "" {
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1, nil
}))
}
// DataClient
client, err := data.NewDataClientFromViper()
if err != nil {
log.Fatalf("Could not establish database connection.\n Error: %s\n", err)
}
defer client.DB.Close()
err = client.DB.Ping()
if err != nil {
log.Fatalf("Database was supposed to be ready but Ping() failed.\n Error: %s\n", err)
}
_, err = client.Seed()
if err != nil {
log.Fatalf("Error during client.Seed(): %s", err)
}
h := handlers.New(client)
// Routes
e.GET("/v5/health/", h.GetHealth)
e.POST("/v5/membres/", h.PostMembres)
e.GET("/v5/membres/", h.ListMembres)
e.GET("/v5/membres/:membre_id/", h.ReadMembre)
e.PUT("/v5/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
e.POST("/v5/programmes/", h.PostProgrammes)
e.POST("/v5/seed/", h.PostSeed)
// Execution
e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort)))
},
}
func init() {
rootCmd.AddCommand(apiCmd)
// api.key
serpents.String(apiCmd.Flags(),
"api.key", "api-key", "bottin",
"API server key. Leave empty for no key auth")
// api.port
serpents.Int(apiCmd.Flags(),
"api.port", "api-port", 1312,
"API server port")
// db.database
serpents.String(apiCmd.Flags(),
"db.database", "db-database", "bottin",
"Postgres database")
// db.host
serpents.String(apiCmd.Flags(),
"db.host", "db-host", "db",
"Postgres host")
// db.password
serpents.String(apiCmd.Flags(),
"db.password", "db-password", "bottin",
"Postgres password")
// db.port
serpents.Int(apiCmd.Flags(),
"db.port", "db-port", 5432,
"Postgres port")
// db.user
serpents.String(apiCmd.Flags(),
"db.user", "db-user", "bottin",
"Postgres user")
}

717
cmd/bottin/main.go Normal file
View file

@ -0,0 +1,717 @@
package main
import (
"context"
"crypto/subtle"
"crypto/tls"
"fmt"
"log"
"net/http"
"os"
"strings"
"codeberg.org/vlbeaudoin/voki/v3"
"git.agecem.com/agecem/bottin/v9/pkg/bottin"
"git.agecem.com/agecem/bottin/v9/templates"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)
// Search config in home directory with name ".bottin" (without extension).
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".bottin")
}
viper.SetEnvPrefix("BOTTIN")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
func main() {
/* TODO
if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil {
log.Fatal(err)
}
*/
// Handle the command-line via cobra and viper
execute()
}
func init() {
// rootCmd
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
// client.api.host
rootCmd.PersistentFlags().String(
"client-api-host",
"api",
"API server host",
)
if err := viper.BindPFlag(
"client.api.host",
rootCmd.PersistentFlags().Lookup("client-api-host"),
); err != nil {
log.Fatal(err)
}
// client.api.key
rootCmd.PersistentFlags().String(
"client-api-key",
"bottin",
"API server key",
)
if err := viper.BindPFlag(
"client.api.key",
rootCmd.PersistentFlags().Lookup("client-api-key"),
); err != nil {
log.Fatal(err)
}
// client.api.port
rootCmd.PersistentFlags().Int(
"client-api-port",
1312,
"API server port",
)
if err := viper.BindPFlag(
"client.api.port",
rootCmd.PersistentFlags().Lookup("client-api-port"),
); err != nil {
log.Fatal(err)
}
// client.api.protocol
rootCmd.PersistentFlags().String(
"client-api-protocol",
"https",
"API server protocol",
)
if err := viper.BindPFlag(
"client.api.protocol",
rootCmd.PersistentFlags().Lookup("client-api-protocol"),
); err != nil {
log.Fatal(err)
}
// server
rootCmd.AddCommand(serverCmd)
// server api
serverCmd.AddCommand(apiCmd)
// server api db
// server.api.db.database
apiCmd.PersistentFlags().String(
"server-api-db-database",
"bottin",
"Postgres database name",
)
if err := viper.BindPFlag(
"server.api.db.database",
apiCmd.PersistentFlags().Lookup("server-api-db-database"),
); err != nil {
log.Fatal(err)
}
// server.api.db.host
apiCmd.PersistentFlags().String(
"server-api-db-host",
"db",
"Postgres host name",
)
if err := viper.BindPFlag(
"server.api.db.host",
apiCmd.PersistentFlags().Lookup("server-api-db-host"),
); err != nil {
log.Fatal(err)
}
// server.api.db.password
apiCmd.PersistentFlags().String(
"server-api-db-password",
"bottin",
"Postgres password",
)
if err := viper.BindPFlag(
"server.api.db.password",
apiCmd.PersistentFlags().Lookup("server-api-db-password"),
); err != nil {
log.Fatal(err)
}
// server.api.db.port
apiCmd.PersistentFlags().Int(
"server-api-db-port",
5432,
"Postgres port",
)
if err := viper.BindPFlag(
"server.api.db.port",
apiCmd.PersistentFlags().Lookup("server-api-db-port"),
); err != nil {
log.Fatal(err)
}
// server.api.db.sslmode
apiCmd.PersistentFlags().String(
"server-api-db-sslmode",
"prefer",
"Postgres sslmode",
)
if err := viper.BindPFlag(
"server.api.db.sslmode",
apiCmd.PersistentFlags().Lookup("server-api-db-sslmode"),
); err != nil {
log.Fatal(err)
}
// server.api.db.user
apiCmd.PersistentFlags().String(
"server-api-db-user",
"bottin",
"Postgres user name",
)
if err := viper.BindPFlag(
"server.api.db.user",
apiCmd.PersistentFlags().Lookup("server-api-db-user"),
); err != nil {
log.Fatal(err)
}
// server.api.host
apiCmd.PersistentFlags().String(
"server-api-host",
"",
"API server hostname or IP to answer on (empty = any)",
)
if err := viper.BindPFlag(
"server.api.host",
apiCmd.PersistentFlags().Lookup("server-api-host"),
); err != nil {
log.Fatal(err)
}
// server.api.key
apiCmd.PersistentFlags().String(
"server-api-key",
"bottin",
"API server key",
)
if err := viper.BindPFlag(
"server.api.key",
apiCmd.PersistentFlags().Lookup("server-api-key"),
); err != nil {
log.Fatal(err)
}
// server.api.port
apiCmd.PersistentFlags().Int(
"server-api-port",
1312,
"API server port",
)
if err := viper.BindPFlag(
"server.api.port",
apiCmd.PersistentFlags().Lookup("server-api-port"),
); err != nil {
log.Fatal(err)
}
// server api tls
// server.api.tls.enabled
apiCmd.PersistentFlags().Bool(
"server-api-tls-enabled",
true,
"Use TLS for API server connections (requires certfile and keyfile)",
)
if err := viper.BindPFlag(
"server.api.tls.enabled",
apiCmd.PersistentFlags().Lookup("server-api-tls-enabled"),
); err != nil {
log.Fatal(err)
}
// server.api.tls.certfile
apiCmd.PersistentFlags().String(
"server-api-tls-certfile",
"/etc/bottin/cert.pem",
"Path to certificate file",
)
if err := viper.BindPFlag(
"server.api.tls.certfile",
apiCmd.PersistentFlags().Lookup("server-api-tls-certfile"),
); err != nil {
log.Fatal(err)
}
// server.api.tls.keyfile
apiCmd.PersistentFlags().String(
"server-api-tls-keyfile",
"/etc/bottin/key.pem",
"Path to private key file",
)
if err := viper.BindPFlag(
"server.api.tls.keyfile",
apiCmd.PersistentFlags().Lookup("server-api-tls-keyfile"),
); err != nil {
log.Fatal(err)
}
// server ui
serverCmd.AddCommand(uiCmd)
// server ui api
// server.ui.api.host
uiCmd.PersistentFlags().String(
"server-ui-api-host",
"api",
"Web UI backend API server host name",
)
if err := viper.BindPFlag(
"server.ui.api.host",
uiCmd.PersistentFlags().Lookup("server-ui-api-host"),
); err != nil {
log.Fatal(err)
}
// server.ui.api.key
uiCmd.PersistentFlags().String(
"server-ui-api-key",
"bottin",
"Web UI backend API server key",
)
if err := viper.BindPFlag(
"server.ui.api.key",
uiCmd.PersistentFlags().Lookup("server-ui-api-key"),
); err != nil {
log.Fatal(err)
}
// server.ui.api.port
uiCmd.PersistentFlags().Int(
"server-ui-api-port",
1312,
"Web UI backend API server port",
)
if err := viper.BindPFlag(
"server.ui.api.port",
uiCmd.PersistentFlags().Lookup("server-ui-api-port"),
); err != nil {
log.Fatal(err)
}
// server.ui.api.protocol
uiCmd.PersistentFlags().String(
"server-ui-api-protocol",
"https",
"Web UI backend API server protocol",
)
if err := viper.BindPFlag(
"server.ui.api.protocol",
uiCmd.PersistentFlags().Lookup("server-ui-api-protocol"),
); err != nil {
log.Fatal(err)
}
// server.ui.api.tls.skipverify
uiCmd.PersistentFlags().Bool(
"server-ui-api-tls-skipverify",
false,
"Skip API server TLS certificate verification",
)
if err := viper.BindPFlag(
"server.ui.api.tls.skipverify",
uiCmd.PersistentFlags().Lookup("server-ui-api-tls-skipverify"),
); err != nil {
log.Fatal(err)
}
// server.ui.host
uiCmd.PersistentFlags().String(
"server-ui-host",
"",
"Web UI host",
)
if err := viper.BindPFlag(
"server.ui.host",
uiCmd.PersistentFlags().Lookup("server-ui-host"),
); err != nil {
log.Fatal(err)
}
// server.ui.password
uiCmd.PersistentFlags().String(
"server-ui-password",
"bottin",
"Web UI password",
)
if err := viper.BindPFlag(
"server.ui.password",
uiCmd.PersistentFlags().Lookup("server-ui-password"),
); err != nil {
log.Fatal(err)
}
// server.ui.port
uiCmd.PersistentFlags().Int(
"server-ui-port",
2312,
"Web UI port",
)
if err := viper.BindPFlag(
"server.ui.port",
uiCmd.PersistentFlags().Lookup("server-ui-port"),
); err != nil {
log.Fatal(err)
}
// server.ui.user
uiCmd.PersistentFlags().String(
"server-ui-user",
"bottin",
"Web UI user",
)
if err := viper.BindPFlag(
"server.ui.user",
uiCmd.PersistentFlags().Lookup("server-ui-user"),
); err != nil {
log.Fatal(err)
}
// server ui tls
// server.ui.tls.enabled
uiCmd.PersistentFlags().Bool(
"server-ui-tls-enabled",
true,
"Web UI enable TLS (requires certfile and keyfile)",
)
if err := viper.BindPFlag(
"server.ui.tls.enabled",
uiCmd.PersistentFlags().Lookup("server-ui-tls-enabled"),
); err != nil {
log.Fatal(err)
}
// server.ui.tls.certfile
uiCmd.PersistentFlags().String(
"server-ui-tls-certfile",
"/etc/bottin/cert.pem",
"Path to Web UI TLS certificate file",
)
if err := viper.BindPFlag(
"server.ui.tls.certfile",
uiCmd.PersistentFlags().Lookup("server-ui-tls-certfile"),
); err != nil {
log.Fatal(err)
}
// server.ui.tls.keyfile
uiCmd.PersistentFlags().String(
"server-ui-tls-keyfile",
"/etc/bottin/key.pem",
"Path to Web UI TLS private key file",
)
if err := viper.BindPFlag(
"server.ui.tls.keyfile",
uiCmd.PersistentFlags().Lookup("server-ui-tls-keyfile"),
); err != nil {
log.Fatal(err)
}
}
/* TODO
func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error {
return fmt.Errorf("not implemented")
}
*/
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "bottin",
Short: "Bottin étudiant de l'AGECEM",
}
// execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
var serverCmd = &cobra.Command{
Use: "server",
Short: "Démarrer serveurs (API ou Web UI)",
}
// apiCmd represents the api command
var apiCmd = &cobra.Command{
Use: "api",
Short: "Démarrer le serveur API",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
var cfg bottin.Config
if err := viper.Unmarshal(&cfg); err != nil {
log.Fatal("parse config:", err)
}
e := echo.New()
// Middlewares
e.Pre(middleware.AddTrailingSlash())
if cfg.Server.API.Key != "" {
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.API.Key)) == 1, nil
}))
} else {
log.Println("Server started but no API key (server.api.key) was provided, using empty key (NOT RECOMMENDED FOR PRODUCTION)")
}
// DataClient
ctx := context.Background()
//prep
pool, err := pgxpool.New(
ctx,
fmt.Sprintf(
"user=%s password=%s database=%s host=%s port=%d sslmode=%s ",
cfg.Server.API.DB.User,
cfg.Server.API.DB.Password,
cfg.Server.API.DB.Database,
cfg.Server.API.DB.Host,
cfg.Server.API.DB.Port,
cfg.Server.API.DB.SSLMode,
))
if err != nil {
log.Fatal("init pgx pool:", err)
}
defer pool.Close()
db := &bottin.PostgresClient{
Ctx: ctx,
Pool: pool,
}
if err := db.Pool.Ping(ctx); err != nil {
log.Fatal("ping db:", err)
}
if err := db.CreateOrReplaceSchema(); err != nil {
log.Fatal("create or replace schema:", err)
}
if err := db.CreateOrReplaceViews(); err != nil {
log.Fatal("create or replace views:", err)
}
// Routes
if err := bottin.AddRoutes(e, db, cfg); err != nil {
log.Fatal("add routes:", err)
}
/*
h := handlers.New(client)
e.GET("/v9/health/", h.GetHealth)
e.POST("/v9/membres/", h.PostMembres)
e.GET("/v9/membres/", h.ListMembres)
e.GET("/v9/membres/:membre_id/", h.ReadMembre)
e.PUT("/v9/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
e.POST("/v9/programmes/", h.PostProgrammes)
e.POST("/v9/seed/", h.PostSeed)
*/
// Execution
switch cfg.Server.API.TLS.Enabled {
case false:
e.Logger.Fatal(
e.Start(
fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port),
),
)
case true:
if cfg.Server.API.TLS.Certfile == "" {
log.Fatal("TLS enabled for API but no certificate file provided")
}
if cfg.Server.API.TLS.Keyfile == "" {
log.Fatal("TLS enabled for UI but no private key file provided")
}
e.Logger.Fatal(
e.StartTLS(
fmt.Sprintf("%s:%d", cfg.Server.API.Host, cfg.Server.API.Port),
cfg.Server.API.TLS.Certfile,
cfg.Server.API.TLS.Keyfile,
),
)
}
},
}
// uiCmd represents the ui command
var uiCmd = &cobra.Command{
Use: "ui",
Aliases: []string{"web", "interface"},
Short: "Démarrer l'interface Web UI",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
// Parse config
var cfg bottin.Config
if err := viper.Unmarshal(&cfg); err != nil {
log.Fatal("init config:", err)
}
e := echo.New()
// Middlewares
// Trailing slash
e.Pre(middleware.AddTrailingSlash())
// Auth
e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) {
usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Server.UI.User)) == 1
passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Server.UI.Password)) == 1
return usersMatch && passwordsMatch, nil
}))
// Templating
e.Renderer = templates.NewTemplate()
// API Client
var httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: cfg.Server.UI.API.TLS.SkipVerify,
},
},
}
apiClient := bottin.APIClient{
Voki: voki.New(
httpClient,
cfg.Server.UI.API.Host,
cfg.Server.UI.API.Key,
cfg.Server.UI.API.Port,
cfg.Server.UI.API.Protocol,
)}
defer apiClient.Voki.CloseIdleConnections()
// Routes
e.GET("/", func(c echo.Context) error {
pingResult, err := apiClient.GetHealth()
if err != nil {
return c.Render(
http.StatusOK,
"index-html",
voki.MessageResponse{Message: fmt.Sprintf("impossible d'accéder au serveur API: %s", err)},
)
}
return c.Render(
http.StatusOK,
"index-html",
voki.MessageResponse{Message: pingResult},
)
})
e.GET("/membre/", func(c echo.Context) error {
membreID := c.QueryParam("membre_id")
switch {
case membreID == "":
return c.Render(
http.StatusOK,
"index-html",
voki.MessageResponse{Message: "❗Veuillez entrer un numéro étudiant à rechercher"},
)
case !bottin.IsMembreID(membreID):
return c.Render(
http.StatusOK,
"index-html",
voki.MessageResponse{Message: fmt.Sprintf("❗Numéro étudiant '%s' invalide", membreID)},
)
}
membre, err := apiClient.GetMembreForDisplay(membreID)
if err != nil {
return c.Render(
http.StatusOK,
"index-html",
voki.MessageResponse{Message: fmt.Sprintf("❗erreur: %s", err)},
)
}
return c.Render(
http.StatusOK,
"index-html",
voki.MessageResponse{Message: fmt.Sprintf(`
Numéro étudiant: %s
Nom d'usage: %s
Programme: [%s] %s
`,
membre.ID,
membre.Name,
membre.ProgrammeID,
membre.ProgrammeName,
)},
)
})
// Execution
switch cfg.Server.UI.TLS.Enabled {
case false:
e.Logger.Fatal(e.Start(
fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port)))
case true:
if cfg.Server.UI.TLS.Certfile == "" {
log.Fatal("TLS enabled for UI but no certificate file provided")
}
if cfg.Server.UI.TLS.Keyfile == "" {
log.Fatal("TLS enabled for UI but no private key file provided")
}
e.Logger.Fatal(
e.StartTLS(
fmt.Sprintf("%s:%d", cfg.Server.UI.Host, cfg.Server.UI.Port),
cfg.Server.UI.TLS.Certfile,
cfg.Server.UI.TLS.Keyfile,
),
)
}
},
}

156
cmd/bottin/main_test.go Normal file
View file

@ -0,0 +1,156 @@
package main
import (
"crypto/tls"
"net/http"
"testing"
"codeberg.org/vlbeaudoin/voki/v3"
"git.agecem.com/agecem/bottin/v9/pkg/bottin"
"github.com/spf13/viper"
)
func init() {
initConfig()
}
func TestAPI(t *testing.T) {
var cfg bottin.Config
if err := viper.Unmarshal(&cfg); err != nil {
t.Error(err)
return
}
//httpClient := http.DefaultClient
//defer httpClient.CloseIdleConnections()
transport := http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
httpClient := http.Client{
Transport: &transport,
}
defer httpClient.CloseIdleConnections()
vokiClient := voki.New(&httpClient, "localhost", cfg.Client.API.Key, cfg.Client.API.Port, cfg.Client.API.Protocol)
apiClient := bottin.APIClient{Voki: vokiClient}
t.Run("get API health", func(t *testing.T) {
health, err := apiClient.GetHealth()
if err != nil {
t.Error(err)
}
want := "ok"
got := health
if want != got {
t.Errorf("want=%s got=%s", want, got)
}
})
t.Run("insert programmes",
func(t *testing.T) {
programmes := []bottin.Programme{
{ID: "404.42", Name: "Cool programme"},
{ID: "200.10", Name: "Autre programme"},
}
t.Log("programmes:", programmes)
_, err := apiClient.InsertProgrammes(programmes...)
if err != nil {
t.Error(err)
}
})
testMembres := []bottin.Membre{
{
ID: "0000000",
FirstName: "Test",
LastName: "User",
ProgrammeID: "404.42",
},
{
ID: "1234567",
FirstName: "Deadname",
LastName: "User",
PreferedName: "User, Test-Name",
ProgrammeID: "200.10",
},
}
t.Run("get programmes, max 50",
func(t *testing.T) {
programmes, err := apiClient.GetProgrammes(50)
if err != nil {
t.Error(err)
}
t.Log(programmes)
})
t.Run("insert membres",
func(t *testing.T) {
_, err := apiClient.InsertMembres(testMembres...)
if err != nil {
t.Error(err)
}
})
t.Run("get membre",
func(t *testing.T) {
membre, err := apiClient.GetMembre(testMembres[0].ID)
if err != nil {
t.Error(err)
}
want := testMembres[0].LastName
got := membre.LastName
if want != got {
t.Errorf("want=%s got=%s", want, got)
}
})
t.Run("get invalid membre",
func(t *testing.T) {
_, err := apiClient.GetMembre("invalid")
if err == nil {
t.Error("`invalid` should not have been accepted as value to GetMembre, but did")
}
})
t.Run("update membre prefered name",
func(t *testing.T) {
if err := apiClient.UpdateMembrePreferedName(testMembres[0].ID, "User, Galaxy"); err != nil {
t.Error(err)
}
})
t.Run("get membres, max 50",
func(t *testing.T) {
membres, err := apiClient.GetMembres(50)
if err != nil {
t.Error(err)
}
t.Log(membres)
})
t.Run("get membre for display",
func(t *testing.T) {
membre, err := apiClient.GetMembreForDisplay(testMembres[0].ID)
if err != nil {
t.Error(err)
}
t.Log(membre)
})
t.Run("get membres for display, max 5",
func(t *testing.T) {
membres, err := apiClient.GetMembresForDisplay(5)
if err != nil {
t.Error(err)
}
t.Log(membres)
})
}

View file

@ -1,56 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "bottin",
Short: "Application de gestion de distribution d'agendas",
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)
// Search config in home directory with name ".bottin" (without extension).
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".bottin")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}

View file

@ -1,142 +0,0 @@
package cmd
import (
"crypto/subtle"
"embed"
"fmt"
"html/template"
"io"
"log"
"net/http"
"codeberg.org/vlbeaudoin/serpents"
"git.agecem.com/agecem/bottin/v5/data"
"git.agecem.com/agecem/bottin/v5/web"
"git.agecem.com/agecem/bottin/v5/web/webhandlers"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
webUser string
webPassword string
webPort int
webApiHost string
webApiKey string
webApiPort int
webApiProtocol string
)
var templatesFS embed.FS
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
// webCmd represents the web command
var webCmd = &cobra.Command{
Use: "web",
Short: "Démarrer le client web",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
webApiHost = viper.GetString("web.api.host")
webApiKey = viper.GetString("web.api.key")
webApiPort = viper.GetInt("web.api.port")
webApiProtocol = viper.GetString("web.api.protocol")
webPassword = viper.GetString("web.password")
webPort = viper.GetInt("web.port")
webUser = viper.GetString("web.user")
// Ping API server
client := http.DefaultClient
defer client.CloseIdleConnections()
apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort)
pingResult, err := apiClient.GetHealth()
if err != nil {
log.Fatal(err)
}
log.Println(pingResult)
e := echo.New()
// Middlewares
e.Pre(middleware.AddTrailingSlash())
e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) {
usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(webUser)) == 1
passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(webPassword)) == 1
return usersMatch && passwordsMatch, nil
}))
// Template
t := &Template{
templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")),
}
e.Renderer = t
// Routes
handler := webhandlers.Handler{APIClient: apiClient}
e.GET("/", handler.GetIndex)
e.GET("/membre/", handler.GetMembre)
// Execution
e.Logger.Fatal(e.Start(
fmt.Sprintf(":%d", webPort)))
},
}
func init() {
rootCmd.AddCommand(webCmd)
templatesFS = web.GetTemplates()
// web.api.host
serpents.String(webCmd.Flags(),
"web.api.host", "web-api-host", "api",
"Remote API server host")
// web.api.key
serpents.String(webCmd.Flags(),
"web.api.key", "web-api-key", "bottin",
"Remote API server key")
// web.api.protocol
serpents.String(webCmd.Flags(),
"web.api.protocol", "web-api-protocol", "http",
"Remote API server protocol")
// web.api.port
serpents.Int(webCmd.Flags(),
"web.api.port", "web-api-port", 1312,
"Remote API server port")
// web.password
serpents.String(webCmd.Flags(),
"web.password", "web-password", "bottin",
"Web client password")
// web.port
serpents.Int(webCmd.Flags(),
"web.port", "web-port", 2312,
"Web client port")
// web.user
serpents.String(webCmd.Flags(),
"web.user", "web-user", "bottin",
"Web client user")
}

View file

@ -1,11 +1,12 @@
name: 'bottin'
services: services:
db: db:
image: 'docker.io/library/postgres:14.8' image: 'docker.io/library/postgres:16'
environment: environment:
POSTGRES_DATABASE: "${BOTTIN_POSTGRES_DATABASE}" POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:?}"
POSTGRES_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}" POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:?}"
POSTGRES_USER: "${BOTTIN_POSTGRES_USER}" POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER:?}"
volumes: volumes:
- 'db-data:/var/lib/postgresql/data' - 'db-data:/var/lib/postgresql/data'
restart: 'unless-stopped' restart: 'unless-stopped'
@ -13,26 +14,28 @@ services:
api: api:
depends_on: depends_on:
- db - db
build: . build: ../..
image: 'git.agecem.com/agecem/bottin:latest' image: 'git.agecem.com/agecem/bottin:latest'
env_file: '.env'
ports: ports:
- '1312:1312' - '1312:1312'
volumes: volumes:
- 'api-config:/etc/bottin' - 'api-config:/etc/bottin'
restart: 'unless-stopped' restart: 'unless-stopped'
command: ['bottin', '--config', '/etc/bottin/api.yaml', 'api'] command: ['bottin', '--config', '/etc/bottin/api.yaml', 'server', 'api']
web: ui:
depends_on: depends_on:
- api - api
build: . build: ../..
image: 'git.agecem.com/agecem/bottin:latest' image: 'git.agecem.com/agecem/bottin:latest'
env_file: '.env'
ports: ports:
- '2312:2312' - '2312:2312'
volumes: volumes:
- 'web-config:/etc/bottin' - 'ui-config:/etc/bottin'
restart: 'unless-stopped' restart: 'unless-stopped'
command: ['bottin', '--config', '/etc/bottin/web.yaml', 'web'] command: ['bottin', '--config', '/etc/bottin/ui.yaml', 'server', 'ui']
# adminer: # adminer:
# image: adminer # image: adminer
@ -45,4 +48,4 @@ services:
volumes: volumes:
db-data: db-data:
api-config: api-config:
web-config: ui-config:

View file

@ -1,74 +0,0 @@
package data
import (
"errors"
"fmt"
"net/http"
"codeberg.org/vlbeaudoin/voki"
"git.agecem.com/agecem/bottin/v5/models"
"git.agecem.com/agecem/bottin/v5/responses"
"github.com/spf13/viper"
)
type ApiClient struct {
Voki *voki.Voki
}
func NewApiClientFromViper(client *http.Client) *ApiClient {
apiClientKey := viper.GetString("web.api.key")
apiClientHost := viper.GetString("web.api.host")
apiClientProtocol := viper.GetString("web.api.protocol")
apiClientPort := viper.GetInt("web.api.port")
return NewApiClient(client, apiClientKey, apiClientHost, apiClientProtocol, apiClientPort)
}
func NewApiClient(client *http.Client, key, host, protocol string, port int) *ApiClient {
return &ApiClient{
Voki: voki.New(client, host, key, port, protocol),
}
}
// GetHealth allows checking for API server health
func (a *ApiClient) GetHealth() (string, error) {
var getHealthResponse responses.GetHealthResponse
err := a.Voki.Unmarshal(http.MethodGet, "/v5/health", nil, true, &getHealthResponse)
if err != nil {
return getHealthResponse.Message, err
}
if getHealthResponse.Message == "" {
return getHealthResponse.Message, errors.New("Could not confirm that API server is up, no response message")
}
return getHealthResponse.Message, nil
}
func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) {
var getMembreResponse struct {
Message string `json:"message"`
Data struct {
Membre models.Membre `json:"membre"`
} `json:"data"`
}
if membreID == "" {
return getMembreResponse.Data.Membre, errors.New("Veuillez fournir un numéro étudiant à rechercher")
}
err := a.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v5/membres/%s", membreID), nil, true, &getMembreResponse)
if err != nil {
return getMembreResponse.Data.Membre, err
}
if getMembreResponse.Data.Membre == *new(models.Membre) {
return getMembreResponse.Data.Membre, fmt.Errorf("Ce numéro étudiant ne correspond à aucunE membre")
}
return getMembreResponse.Data.Membre, nil
}
func (a *ApiClient) ListMembres() (r responses.ListMembresResponse, err error) {
return r, a.Voki.Unmarshal(http.MethodGet, "/v5/membres", nil, true, &r)
}

View file

@ -1,188 +0,0 @@
package data
import (
"errors"
"fmt"
"git.agecem.com/agecem/bottin/v5/models"
_ "github.com/jackc/pgx/stdlib"
"github.com/jmoiron/sqlx"
"github.com/spf13/viper"
)
// DataClient is a postgres client based on sqlx
type DataClient struct {
PostgresConnection PostgresConnection
DB sqlx.DB
}
type PostgresConnection struct {
User string
Password string
Database string
Host string
Port int
SSL bool
}
func NewDataClientFromViper() (*DataClient, error) {
client, err := NewDataClient(
PostgresConnection{
User: viper.GetString("db.user"),
Password: viper.GetString("db.password"),
Host: viper.GetString("db.host"),
Database: viper.GetString("db.database"),
Port: viper.GetInt("db.port"),
})
return client, err
}
func NewDataClient(connection PostgresConnection) (*DataClient, error) {
client := &DataClient{PostgresConnection: connection}
connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s",
client.PostgresConnection.User,
client.PostgresConnection.Password,
client.PostgresConnection.Host,
client.PostgresConnection.Port,
client.PostgresConnection.Database,
)
db, err := sqlx.Connect("pgx", connectionString)
if err != nil {
return nil, err
}
client.DB = *db
return client, nil
}
func (d *DataClient) Seed() (int64, error) {
result, err := d.DB.Exec(models.Schema)
if err != nil {
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
return rows, err
}
return rows, nil
}
// InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered
func (d *DataClient) InsertMembres(membres []models.Membre) (int64, error) {
var rowsInserted int64
tx, err := d.DB.Beginx()
if err != nil {
tx.Rollback()
return rowsInserted, err
}
for _, membre := range membres {
if membre.ID == "" {
tx.Rollback()
return 0, errors.New("Cannot insert membre with no membre_id")
}
result, err := tx.NamedExec("INSERT INTO membres (id, last_name, first_name, prefered_name, programme_id) VALUES (:id, :last_name, :first_name, :prefered_name, :programme_id) ON CONFLICT (id) DO NOTHING;", &membre)
if err != nil {
tx.Rollback()
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
tx.Rollback()
return 0, err
}
rowsInserted += rows
}
err = tx.Commit()
if err != nil {
return rowsInserted, err
}
return rowsInserted, nil
}
func (d *DataClient) InsertProgrammes(programmes []models.Programme) (int64, error) {
var rowsInserted int64
tx, err := d.DB.Beginx()
if err != nil {
tx.Rollback()
return rowsInserted, err
}
for _, programme := range programmes {
if programme.ID == "" {
tx.Rollback()
return 0, errors.New("Cannot insert programme with no programme_id")
}
result, err := tx.NamedExec("INSERT INTO programmes (id, titre) VALUES (:id, :titre) ON CONFLICT DO NOTHING;", &programme)
if err != nil {
tx.Rollback()
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
tx.Rollback()
return 0, err
}
rowsInserted += rows
}
err = tx.Commit()
if err != nil {
return rowsInserted, err
}
return rowsInserted, nil
}
func (d *DataClient) GetMembre(membreID string) (models.Membre, error) {
var membre models.Membre
rows, err := d.DB.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID)
if err != nil {
return membre, err
}
for rows.Next() {
err := rows.StructScan(&membre)
if err != nil {
return membre, err
}
}
if membre.ID == "" {
return membre, fmt.Errorf("No membre by that id was found")
}
return membre, nil
}
func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) {
result, err := d.DB.Exec("UPDATE membres SET prefered_name = $1 WHERE id = $2;", newName, membreID)
if err != nil {
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
return rows, err
}
return rows, nil
}
func (d *DataClient) GetMembres() (membres []models.Membre, err error) {
return membres, d.DB.Select(&membres, "SELECT * FROM membres;")
}

View file

@ -0,0 +1,60 @@
apiVersion: v1
kind: Pod
metadata:
name: bottin-pod
spec:
initContainers:
- name: clone
image: alpine:3.20
command: ['sh', '-c']
args:
- apk add git &&
git clone -- https://git.agecem.com/agecem/bottin /opt/bottin-src
volumeMounts:
- name: bottin-src
mountPath: /opt/bottin-src
- name: build
image: golang:1.23
env:
- name: CGO_ENABLED
value: '0'
command: ['sh', '-c']
args:
- cd /opt/bottin-src &&
go build -a -o /opt/bottin-executable/bottin
volumeMounts:
- name: bottin-src
mountPath: /opt/bottin-src
- name: bottin-executable
mountPath: /opt/bottin-executable
containers:
- name: api
image: alpine:3.20
command: ['sh', '-c']
args:
- ln -s /opt/bottin-executable/bottin /usr/bin/bottin
volumeMounts:
- name: bottin-executable
mountPath: /opt/bottin-executable
- name: bottin-secret
readOnly: true
mountPath: '/etc/bottin'
- name: ui
image: alpine:3.20
command: ['sh', '-c']
args:
- bottin --config /etc/bottin/ui.yaml server ui
volumeMounts:
- name: bottin-executable
mountPath: /opt/bottin-executable
- name: bottin-secret
readOnly: true
mountPath: '/etc/bottin'
volumes:
- name: bottin-src
emptyDir: {}
- name: bottin-executable
emptyDir: {}
- name: bottin-secret
secret:
secretName: bottin-secret

View file

@ -0,0 +1,26 @@
apiVersion: v1
kind: Secret
metadata:
name: bottin-secret
stringData:
api.yaml: |
bottin:
server:
api:
db:
database: 'bottin'
host: 'db.example.com'
password: 'bottin'
sslmode: 'require'
user: 'bottin'
key: 'bottin'
ui.yaml: |
bottin:
server:
ui:
api:
tls:
skipverify: 'true'
key: 'bottin'
password: 'bottin'
user: 'bottin'

3
examples/example.csv Normal file
View file

@ -0,0 +1,3 @@
programme_id;nom_programme;
000.00;test programme;
111.11;autre test programme;
1 programme_id nom_programme
2 000.00 test programme
3 111.11 autre test programme

58
go.mod
View file

@ -1,46 +1,48 @@
module git.agecem.com/agecem/bottin/v5 module git.agecem.com/agecem/bottin/v9
go 1.21.1 go 1.22.0
require ( require (
codeberg.org/vlbeaudoin/serpents v1.0.2 codeberg.org/vlbeaudoin/pave/v2 v2.0.0
codeberg.org/vlbeaudoin/voki v1.3.1 codeberg.org/vlbeaudoin/voki/v3 v3.0.0
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
github.com/jackc/pgx v3.6.2+incompatible github.com/jackc/pgx/v5 v5.6.0
github.com/jmoiron/sqlx v1.3.5 github.com/labstack/echo/v4 v4.12.0
github.com/labstack/echo/v4 v4.10.2 github.com/spf13/cobra v1.8.1
github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.19.0
github.com/spf13/viper v1.16.0
) )
require ( require (
github.com/cockroachdb/apd v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/labstack/gommon v0.4.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/spf13/afero v1.9.5 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.5.1 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.10.0 // indirect golang.org/x/crypto v0.24.0 // indirect
golang.org/x/sys v0.8.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/text v0.9.0 // indirect golang.org/x/net v0.26.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

577
go.sum
View file

@ -1,546 +1,115 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= codeberg.org/vlbeaudoin/pave/v2 v2.0.0 h1:hfB5KnqMMu17g5QBWgLvWOsqidrYaohRfu2LflmTrb0=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= codeberg.org/vlbeaudoin/pave/v2 v2.0.0/go.mod h1:TsTfP6IA+3Ph33vLZigeJWS5vgBPgkW1tfs3zFPfycU=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= codeberg.org/vlbeaudoin/voki/v3 v3.0.0 h1:XdF/UTe9YUNj3hYrAyEvdmIMDYLL8SkqTwPkqw1yJ2c=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= codeberg.org/vlbeaudoin/voki/v3 v3.0.0/go.mod h1:+6LMXosAu2ijNKV04sMwkeujpH+cghZU1fydqj2y95g=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
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=
codeberg.org/vlbeaudoin/serpents v1.0.2 h1:mHuL+RBAMOGeiB5+ew1cRputEAnOIQNJW9o9a5Qjudo=
codeberg.org/vlbeaudoin/serpents v1.0.2/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ=
codeberg.org/vlbeaudoin/voki v1.3.1 h1:TxJj3qmOys0Pbq1dPKnOEXMXKqQLQqrBYd4QqiWWXcw=
codeberg.org/vlbeaudoin/voki v1.3.1/go.mod h1:5XTLx/KiW/OfiupF3o7PAAAU/UhsPdKSrVMmtHbmkPI=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
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=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d h1:KbPOUXFUDJxwZ04vbmDOc3yuruGvVO+LOa7cVER3yWw=
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 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/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View file

@ -1,11 +0,0 @@
package handlers
import "git.agecem.com/agecem/bottin/v5/data"
type Handler struct {
DataClient *data.DataClient
}
func New(dataClient *data.DataClient) *Handler {
return &Handler{DataClient: dataClient}
}

View file

@ -1,36 +0,0 @@
package handlers
import (
"net/http"
"git.agecem.com/agecem/bottin/v5/data"
"git.agecem.com/agecem/bottin/v5/responses"
"github.com/labstack/echo/v4"
)
func (h *Handler) GetHealth(c echo.Context) error {
var response responses.GetHealthResponse
dataClient, err := data.NewDataClientFromViper()
if err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Error during data.NewDataClientFromViper()"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
defer dataClient.DB.Close()
if err = dataClient.DB.Ping(); err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Error during dataClient.DB.Ping()"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
response.StatusCode = http.StatusOK
response.Message = "Bottin API v5 is ready"
return c.JSON(response.StatusCode, response)
}

View file

@ -1,129 +0,0 @@
package handlers
import (
"encoding/csv"
"io"
"net/http"
"git.agecem.com/agecem/bottin/v5/models"
"git.agecem.com/agecem/bottin/v5/responses"
"github.com/labstack/echo/v4"
"github.com/gocarina/gocsv"
)
func (h *Handler) PostMembres(c echo.Context) error {
var response responses.PostMembresResponse
var membres []models.Membre
switch c.Request().Header.Get("Content-Type") {
case "application/json":
if err := c.Bind(&membres); err != nil {
response.StatusCode = http.StatusBadRequest
response.Message = "Could not bind membres"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
case "text/csv":
body := c.Request().Body
if body == nil {
response.StatusCode = http.StatusBadRequest
response.Message = "Request body is empty"
return c.JSON(response.StatusCode, response)
}
defer body.Close()
// Parse the CSV data from the request body using gocsv.
if err := gocsv.Unmarshal(body, &membres); err != nil {
response.StatusCode = http.StatusBadRequest
response.Message = "Could not unmarshal into membres"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
default:
response.StatusCode = http.StatusBadRequest
response.Message = "Invalid Content-Type"
return c.JSON(response.StatusCode, response)
}
if len(membres) == 0 {
response.StatusCode = http.StatusOK
response.Message = "Nothing to do"
return c.JSON(response.StatusCode, response)
}
newMembres, err := h.DataClient.InsertMembres(membres)
if err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Could not insert membres"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
response.StatusCode = http.StatusCreated
response.Message = "Insert successful"
response.Data.MembresInserted = newMembres
return c.JSON(response.StatusCode, response)
}
func (h *Handler) PostProgrammes(c echo.Context) error {
var response responses.PostProgrammesResponse
var programmes []models.Programme
switch c.Request().Header.Get("Content-Type") {
case "application/json":
if err := c.Bind(&programmes); err != nil {
response.StatusCode = http.StatusBadRequest
response.Message = "Could not bind programmes"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
case "text/csv":
body := c.Request().Body
if body == nil {
response.StatusCode = http.StatusBadRequest
response.Message = "Request body is empty"
return c.JSON(response.StatusCode, response)
}
defer body.Close()
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
r := csv.NewReader(in)
r.Comma = ';'
return r // Allows use ; as delimiter
})
// Parse the CSV data from the request body using gocsv.
if err := gocsv.Unmarshal(body, &programmes); err != nil {
response.StatusCode = http.StatusBadRequest
response.Message = "Could not unmarshal into programmes"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
default:
response.StatusCode = http.StatusBadRequest
response.Message = "Invalid Content-Type"
return c.JSON(response.StatusCode, response)
}
if len(programmes) == 0 {
response.StatusCode = http.StatusOK
response.Message = "Nothing to do"
return c.JSON(response.StatusCode, response)
}
newProgrammes, err := h.DataClient.InsertProgrammes(programmes)
if err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Could not insert programmes"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
response.StatusCode = http.StatusCreated
response.Message = "Insert successful"
response.Data.ProgrammesInserted = newProgrammes
return c.JSON(response.StatusCode, response)
}

View file

@ -1,61 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"git.agecem.com/agecem/bottin/v5/responses"
"github.com/labstack/echo/v4"
)
func (h *Handler) ReadMembre(c echo.Context) error {
membreID := c.Param("membre_id")
membre, err := h.DataClient.GetMembre(membreID)
if err != nil {
if err.Error() == "No membre by that id was found" {
return c.JSON(http.StatusNotFound, map[string]string{
"message": "Not Found",
})
}
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Unknown error during GetMembre",
"error": err.Error(),
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Read successful",
"data": map[string]interface{}{
"membre": &membre,
},
})
}
func (h *Handler) ListMembres(c echo.Context) error {
var r responses.ListMembresResponse
membres, err := h.DataClient.GetMembres()
if err != nil {
r.StatusCode = http.StatusInternalServerError
r.Error = err.Error()
r.Message = "Error during (*handlers.Handler).DataClient.GetMembres"
return c.JSON(r.StatusCode, r)
}
r.StatusCode = http.StatusOK
switch membres := len(membres); membres {
case 0:
r.Message = "No membres returned from database"
case 1:
r.Message = "Membre returned from database"
default:
r.Message = fmt.Sprintf("%d membres returned from database", membres)
}
r.Data.Membres = membres
return c.JSON(r.StatusCode, r)
}

View file

@ -1,24 +0,0 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v4"
)
func (h *Handler) PostSeed(c echo.Context) error {
rows, err := h.DataClient.Seed()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Seed failed",
"error": err.Error(),
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Seed successful",
"data": map[string]interface{}{
"rows": rows,
},
})
}

View file

@ -1,42 +0,0 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v4"
)
func (h *Handler) PutMembrePreferedName(c echo.Context) error {
membreID := c.Param("membre_id")
var newName string
err := c.Bind(&newName)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"message": "Could not bind newName",
"error": err.Error(),
})
}
rows, err := h.DataClient.UpdateMembreName(membreID, newName)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Could not update membre name",
"error": err.Error(),
})
}
if rows == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{
"message": "No update was done, probably no membre by that id",
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Update successful",
"data": map[string]interface{}{
"rows": rows,
},
})
}

View file

@ -1,7 +0,0 @@
package main
import "git.agecem.com/agecem/bottin/v5/cmd"
func main() {
cmd.Execute()
}

View file

@ -1,33 +0,0 @@
package models
const Schema = `
CREATE TABLE IF NOT EXISTS programmes (
id TEXT PRIMARY KEY,
titre TEXT
);
CREATE TABLE IF NOT EXISTS membres (
id VARCHAR(7) PRIMARY KEY,
last_name TEXT,
first_name TEXT,
prefered_name TEXT,
programme_id TEXT REFERENCES programmes(id)
);
`
type Programme struct {
ID string `db:"id" json:"programme_id" csv:"programme_id"`
Titre string `db:"titre" json:"nom_programme" csv:"nom_programme"`
}
type Membre struct {
ID string `db:"id" json:"membre_id" csv:"membre_id"`
LastName string `db:"last_name" json:"last_name" csv:"last_name"`
FirstName string `db:"first_name" json:"first_name" csv:"first_name"`
PreferedName string `db:"prefered_name" json:"prefered_name" csv:"prefered_name"`
ProgrammeID string `db:"programme_id" json:"programme_id" csv:"programme_id"`
}
type Entry interface {
Programme | Membre
}

170
pkg/bottin/client.go Normal file
View file

@ -0,0 +1,170 @@
package bottin
import (
"fmt"
"codeberg.org/vlbeaudoin/voki/v3"
)
type APIClient struct {
Voki *voki.Voki
}
func (c APIClient) GetHealth() (health string, err error) {
var request HealthGETRequest
response, err := request.Request(c.Voki)
if err != nil {
return "", err
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Message, nil
}
func (c APIClient) InsertProgrammes(programmes ...Programme) (amountInserted int64, err error) {
var request ProgrammesPOSTRequest
request.Data.Programmes = programmes
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.ProgrammesInserted, nil
}
func (c APIClient) InsertMembres(membres ...Membre) (amountInserted int64, err error) {
var request MembresPOSTRequest
request.Data.Membres = membres
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.MembresInserted, nil
}
func (c APIClient) GetMembre(membreID string) (membre Membre, err error) {
var request MembreGETRequest
request.Param.MembreID = membreID
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.Membre, nil
}
func (c APIClient) GetMembres(limit int) (membres []Membre, err error) {
var request MembresGETRequest
request.Query.Limit = limit
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.Membres, nil
}
func (c APIClient) GetProgrammes(limit int) (programmes []Programme, err error) {
var request ProgrammesGETRequest
request.Query.Limit = limit
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.Programmes, nil
}
func (c APIClient) UpdateMembrePreferedName(membreID string, name string) (err error) {
var request MembrePreferedNamePUTRequest
if !IsMembreID(membreID) {
return fmt.Errorf("Numéro étudiant '%s' invalide", membreID)
}
request.Param.MembreID = membreID
request.Data.PreferedName = name
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return nil
}
func (c APIClient) GetMembreForDisplay(membreID string) (membre MembreForDisplay, err error) {
var request MembreDisplayGETRequest
request.Param.MembreID = membreID
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.Membre, nil
}
func (c APIClient) GetMembresForDisplay(limit int) (membres []MembreForDisplay, err error) {
var request MembresDisplayGETRequest
request.Query.Limit = limit
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.Membres, nil
}

53
pkg/bottin/config.go Normal file
View file

@ -0,0 +1,53 @@
package bottin
type Config struct {
Client struct {
API struct {
Host string `yaml:"host"`
Key string `yaml:"key"`
Port int `yaml:"port"`
Protocol string `yaml:"protocol"`
} `yaml:"api"`
} `yaml:"client"`
Server struct {
API struct {
DB struct {
Database string `yaml:"database"`
Host string `yaml:"host"`
Password string `yaml:"password"`
Port int `yaml:"port"`
SSLMode string `yaml:"sslmode"`
User string `yaml:"user"`
} `yaml:"db"`
Host string `yaml:"host"`
Key string `yaml:"key"`
Port int `yaml:"port"`
TLS struct {
Enabled bool `yaml:"enabled"`
Certfile string `yaml:"certfile"`
Keyfile string `yaml:"keyfile"`
} `yaml:"tls"`
} `yaml:"api"`
UI struct {
API struct {
Host string `yaml:"host"`
Key string `yaml:"key"`
Port int `yaml:"port"`
Protocol string `yaml:"protocol"`
TLS struct {
SkipVerify bool `yaml:"skipverify"`
} `yaml:"tls"`
} `yaml:"api"`
Host string `yaml:"host"`
Password string `yaml:"password"`
Port int `yaml:"port"`
TLS struct {
Enabled bool `yaml:"enabled"`
Certfile string `yaml:"certfile"`
Keyfile string `yaml:"keyfile"`
} `yaml:"tls"`
User string `yaml:"user"`
} `yaml:"ui"`
} `yaml:"server"`
}

353
pkg/bottin/db.go Normal file
View file

@ -0,0 +1,353 @@
package bottin
import (
"context"
_ "embed"
"fmt"
"git.agecem.com/agecem/bottin/v9/queries"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type PostgresClient struct {
//TODO move context out of client
Ctx context.Context
Pool *pgxpool.Pool
}
func (db *PostgresClient) CreateOrReplaceSchema() error {
_, err := db.Pool.Exec(db.Ctx, queries.SQLSchema())
return err
}
func (db *PostgresClient) CreateOrReplaceViews() error {
_, err := db.Pool.Exec(db.Ctx, queries.SQLViews())
return err
}
// InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered
func (d *PostgresClient) InsertMembres(membres ...Membre) (inserted int64, err error) {
select {
case <-d.Ctx.Done():
return inserted, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
tx, err := d.Pool.Begin(d.Ctx)
if err != nil {
return inserted, err
}
defer tx.Rollback(d.Ctx)
for i, membre := range membres {
if membre.ID == "" {
return inserted, fmt.Errorf("insertion ligne %d: membre requiert numéro étudiant valide", i)
}
result, err := tx.Exec(d.Ctx, `
INSERT INTO membres
(id, last_name, first_name, prefered_name, programme_id)
VALUES
($1, $2, $3, $4, $5)
ON CONFLICT (id) DO NOTHING;`,
membre.ID,
membre.LastName,
membre.FirstName,
membre.PreferedName,
membre.ProgrammeID,
)
if err != nil {
return 0, err
}
inserted += result.RowsAffected()
}
if err = tx.Commit(d.Ctx); err != nil {
return 0, err
}
return inserted, err
}
}
func (d *PostgresClient) InsertProgrammes(programmes ...Programme) (inserted int64, err error) {
select {
case <-d.Ctx.Done():
return inserted, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
tx, err := d.Pool.Begin(d.Ctx)
if err != nil {
return inserted, err
}
defer tx.Rollback(d.Ctx)
for _, programme := range programmes {
if programme.ID == "" {
return 0, fmt.Errorf("Cannot insert programme with no programme_id")
}
result, err := tx.Exec(d.Ctx, `
INSERT INTO programmes
(id, name)
VALUES ($1, $2) ON CONFLICT DO NOTHING;`,
programme.ID,
programme.Name)
if err != nil {
return 0, err
}
inserted += result.RowsAffected()
}
if err := tx.Commit(d.Ctx); err != nil {
return inserted, err
}
return inserted, err
}
}
func (d *PostgresClient) GetMembre(membreID string) (membre Membre, err error) {
select {
case <-d.Ctx.Done():
err = fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
return
default:
if err = d.Pool.QueryRow(d.Ctx, `
SELECT
"membres".id,
"membres".last_name,
"membres".first_name,
"membres".prefered_name,
"membres".programme_id
FROM
"membres"
WHERE
"membres".id = $1
LIMIT
1;
`, membreID).Scan(
&membre.ID,
&membre.LastName,
&membre.FirstName,
&membre.PreferedName,
&membre.ProgrammeID,
); err != nil {
return
}
if membre.ID == "" {
return membre, fmt.Errorf("Aucun membre trouvé avec numéro '%s'", membre.ID)
}
return membre, nil
}
}
/*
func (d *PostgresClient) UpdateMembreName(membreID, newName string) (int64, error) {
result, err := d.Pool.Exec("UPDATE membres SET prefered_name = $1 WHERE id = $2;", newName, membreID)
if err != nil {
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
return rows, err
}
return rows, nil
}
*/
func (d *PostgresClient) GetMembres(limit int) (membres []Membre, err error) {
select {
case <-d.Ctx.Done():
return nil, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
rows, err := d.Pool.Query(d.Ctx, `
SELECT
"membres".id,
"membres".last_name,
"membres".first_name,
"membres".prefered_name,
"membres".programme_id
FROM
"membres"
ORDER BY
"membres".id
LIMIT
$1;
`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var membre Membre
if err = rows.Scan(
&membre.ID,
&membre.LastName,
&membre.FirstName,
&membre.PreferedName,
&membre.ProgrammeID,
); err != nil {
return nil, err
}
membres = append(membres, membre)
}
if rows.Err() != nil {
return membres, rows.Err()
}
return membres, nil
}
}
func (d *PostgresClient) GetProgrammes(limit int) (programmes []Programme, err error) {
select {
case <-d.Ctx.Done():
return nil, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
rows, err := d.Pool.Query(d.Ctx, `
SELECT
"programmes".id,
"programmes".name
FROM
"programmes"
ORDER BY
"programmes".id
LIMIT
$1;
`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var programme Programme
if err = rows.Scan(
&programme.ID,
&programme.Name,
); err != nil {
return nil, err
}
programmes = append(programmes, programme)
}
if rows.Err() != nil {
return programmes, rows.Err()
}
return programmes, nil
}
}
func (d *PostgresClient) UpdateMembrePreferedName(membreID string, name string) (err error) {
select {
case <-d.Ctx.Done():
return fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
if !IsMembreID(membreID) {
return fmt.Errorf("Numéro étudiant '%s' invalide", membreID)
}
_, err = d.Pool.Exec(d.Ctx, `
UPDATE
"membres"
SET
prefered_name = $1
WHERE
"membres".id = $2;
`, name, membreID)
}
return
}
func (d *PostgresClient) GetMembreForDisplay(membreID string) (membre MembreForDisplay, err error) {
select {
case <-d.Ctx.Done():
err = fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
return
default:
if err = d.Pool.QueryRow(d.Ctx, `
SELECT
"membres_for_display".id,
"membres_for_display".name,
"membres_for_display".programme_id,
"membres_for_display".programme_name
FROM
"membres_for_display"
WHERE
"membres_for_display".id = $1
LIMIT
1;
`, membreID).Scan(
&membre.ID,
&membre.Name,
&membre.ProgrammeID,
&membre.ProgrammeName,
); err != nil {
if err == pgx.ErrNoRows {
err = fmt.Errorf("Numéro étudiant valide mais aucun·e membre trouvé·e")
}
return
}
if membre.ID == "" {
return membre, fmt.Errorf("Aucun membre trouvé avec numéro '%s'", membre.ID)
}
return membre, nil
}
}
func (d *PostgresClient) GetMembresForDisplay(limit int) (membres []MembreForDisplay, err error) {
select {
case <-d.Ctx.Done():
return nil, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
rows, err := d.Pool.Query(d.Ctx, `
SELECT
"membres_for_display".id,
"membres_for_display".name,
"membres_for_display".programme_id,
"membres_for_display".programme_name
FROM
"membres_for_display"
ORDER BY
"membres_for_display".id
LIMIT
$1;
`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var membre MembreForDisplay
if err = rows.Scan(
&membre.ID,
&membre.Name,
&membre.ProgrammeID,
&membre.ProgrammeName,
); err != nil {
return nil, err
}
membres = append(membres, membre)
}
if rows.Err() != nil {
return membres, rows.Err()
}
return membres, nil
}
}

38
pkg/bottin/entity.go Normal file
View file

@ -0,0 +1,38 @@
package bottin
import "unicode"
type Programme struct {
ID string `db:"id" json:"programme_id" csv:"programme_id"`
Name string `db:"name" json:"nom_programme" csv:"nom_programme"`
}
type Membre struct {
ID string `db:"id" json:"membre_id" csv:"membre_id"`
LastName string `db:"last_name" json:"last_name" csv:"last_name"`
FirstName string `db:"first_name" json:"first_name" csv:"first_name"`
PreferedName string `db:"prefered_name" json:"prefered_name" csv:"prefered_name"`
ProgrammeID string `db:"programme_id" json:"programme_id" csv:"programme_id"`
}
// MembreForDisplay maps to the `membres_for_display` view declared in `sql/views.sql`
type MembreForDisplay struct {
ID string `db:"id" json:"membre_id" csv:"membre_id"`
Name string `db:"name" json:"name" csv:"name"`
ProgrammeID string `db:"programme_id" json:"programme_id" csv:"programme_id"`
ProgrammeName string `db:"programme_name" json:"programme_name" csv:"programme_name"`
}
func IsMembreID(membre_id string) bool {
if len(membre_id) != 7 {
return false
}
for _, character := range membre_id {
if !unicode.IsDigit(character) {
return false
}
}
return true
}

350
pkg/bottin/request.go Normal file
View file

@ -0,0 +1,350 @@
package bottin
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"codeberg.org/vlbeaudoin/voki/v3"
)
var _ voki.Requester[HealthGETResponse] = HealthGETRequest{}
type HealthGETRequest struct{}
func (request HealthGETRequest) Complete() bool { return true }
func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete HealthGET request")
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
"/api/v9/health/",
nil,
true,
)
if err != nil {
err = fmt.Errorf("%d: %s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[ProgrammesPOSTResponse] = ProgrammesPOSTRequest{}
type ProgrammesPOSTRequest struct {
Data struct {
Programmes []Programme
}
}
func (request ProgrammesPOSTRequest) Complete() bool {
return len(request.Data.Programmes) != 0
}
func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesPOSTResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete ProgrammesPOSTRequest")
return
}
var buf bytes.Buffer
if err = json.NewEncoder(&buf).Encode(request.Data); err != nil {
return
}
statusCode, body, err := v.CallAndParse(
http.MethodPost,
"/api/v9/programme/",
&buf,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembresPOSTResponse] = MembresPOSTRequest{}
type MembresPOSTRequest struct {
Data struct {
Membres []Membre
}
}
func (request MembresPOSTRequest) Complete() bool {
return len(request.Data.Membres) != 0
}
func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembresPOSTRequest")
return
}
var buf bytes.Buffer
if err = json.NewEncoder(&buf).Encode(request.Data); err != nil {
return
}
statusCode, body, err := v.CallAndParse(
http.MethodPost,
"/api/v9/membre/",
&buf,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembreGETResponse] = MembreGETRequest{}
type MembreGETRequest struct {
Param struct {
MembreID string `json:"membre_id" param:"membre_id"`
}
}
func (request MembreGETRequest) Complete() bool {
return request.Param.MembreID != ""
}
func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembreGETRequest")
return
}
if id := request.Param.MembreID; !IsMembreID(id) {
err = fmt.Errorf("MembreID '%s' invalide", id)
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
fmt.Sprintf("/api/v9/membre/%s/", request.Param.MembreID),
nil,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembresGETResponse] = MembresGETRequest{}
type MembresGETRequest struct {
Query struct {
Limit int `json:"limit" query:"limit"`
}
}
func (request MembresGETRequest) Complete() bool { return true }
func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembresGETRequest")
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
fmt.Sprintf("/api/v9/membre/?limit=%d", request.Query.Limit),
nil,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembrePreferedNamePUTResponse] = MembrePreferedNamePUTRequest{}
type MembrePreferedNamePUTRequest struct {
Data struct {
PreferedName string `json:"prefered_name"`
} `json:"data"`
Param struct {
MembreID string `json:"membre_id" param:"membre_id"`
} `json:"param"`
}
func (request MembrePreferedNamePUTRequest) Complete() bool {
return IsMembreID(request.Param.MembreID) && len(request.Data.PreferedName) != 0
}
func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response MembrePreferedNamePUTResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembrePreferedNamePUTRequest")
return
}
var buf bytes.Buffer
if err = json.NewEncoder(&buf).Encode(request.Data); err != nil {
return
}
statusCode, body, err := v.CallAndParse(
http.MethodPut,
fmt.Sprintf("/api/v9/membre/%s/prefered_name/", request.Param.MembreID),
&buf,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[ProgrammesGETResponse] = ProgrammesGETRequest{}
type ProgrammesGETRequest struct {
Query struct {
Limit int `json:"limit" query:"limit"`
}
}
func (request ProgrammesGETRequest) Complete() bool { return true }
func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete ProgrammesGETRequest")
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
fmt.Sprintf("/api/v9/programme/?limit=%d", request.Query.Limit),
nil,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembresDisplayGETResponse] = MembresDisplayGETRequest{}
type MembresDisplayGETRequest struct {
Query struct {
Limit int `json:"limit" query:"limit"`
}
}
func (request MembresDisplayGETRequest) Complete() bool { return true }
func (request MembresDisplayGETRequest) Request(v *voki.Voki) (response MembresDisplayGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembresDisplayGETRequest")
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
fmt.Sprintf("/api/v9/membre/display/?limit=%d", request.Query.Limit),
nil,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembreDisplayGETResponse] = MembreDisplayGETRequest{}
type MembreDisplayGETRequest struct {
Param struct {
MembreID string `json:"membre_id" param:"membre_id"`
}
}
func (request MembreDisplayGETRequest) Complete() bool {
return request.Param.MembreID != ""
}
func (request MembreDisplayGETRequest) Request(v *voki.Voki) (response MembreDisplayGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembreDisplayGETRequest")
return
}
if id := request.Param.MembreID; !IsMembreID(id) {
err = fmt.Errorf("MembreID '%s' invalide", id)
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
fmt.Sprintf("/api/v9/membre/%s/display/", request.Param.MembreID),
nil,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}

91
pkg/bottin/response.go Normal file
View file

@ -0,0 +1,91 @@
package bottin
import (
"fmt"
"codeberg.org/vlbeaudoin/voki/v3"
)
type APIResponse struct {
voki.MessageResponse
statusCode int
}
func (R APIResponse) StatusCode() int { return R.statusCode }
func (R *APIResponse) SetStatusCode(code int) error {
if code <= 0 {
return fmt.Errorf("Cannot set status code to %d", code)
}
R.statusCode = code
return nil
}
type HealthGETResponse struct {
APIResponse
}
type MembreGETResponse struct {
APIResponse
Data MembreGETResponseData `json:"data"`
}
type MembreGETResponseData struct {
Membre Membre `json:"membre"`
}
type MembrePreferedNamePUTResponse struct {
APIResponse
}
type MembresGETResponse struct {
APIResponse
Data MembresGETResponseData `json:"data"`
}
type MembresGETResponseData struct {
Membres []Membre `json:"membres"`
}
type MembreDisplayGETResponse struct {
APIResponse
Data MembreDisplayGETResponseData `json:"data"`
}
type MembreDisplayGETResponseData struct {
Membre MembreForDisplay `json:"membre_for_display"`
}
type MembresDisplayGETResponse struct {
APIResponse
Data MembresDisplayGETResponseData `json:"data"`
}
type MembresDisplayGETResponseData struct {
Membres []MembreForDisplay `json:"membres_for_display"`
}
type MembresPOSTResponse struct {
APIResponse
Data MembresPOSTResponseData `json:"data"`
}
type MembresPOSTResponseData struct {
MembresInserted int64 `json:"membres_inserted"`
}
type ProgrammesPOSTResponse struct {
APIResponse
Data ProgrammesPOSTResponseData `json:"data"`
}
type ProgrammesPOSTResponseData struct {
ProgrammesInserted int64 `json:"programmes_inserted"`
}
type ProgrammesGETResponse struct {
APIResponse
Data ProgrammesGETResponseData `json:"data"`
}
type ProgrammesGETResponseData struct {
Programmes []Programme `json:"programmes"`
}

465
pkg/bottin/routes.go Normal file
View file

@ -0,0 +1,465 @@
package bottin
import (
"encoding/csv"
"fmt"
"io"
"net/http"
"strconv"
"codeberg.org/vlbeaudoin/pave/v2"
"codeberg.org/vlbeaudoin/voki/v3"
"github.com/gocarina/gocsv"
"github.com/labstack/echo/v4"
)
func AddRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
_ = db
_ = cfg
apiPath := "/api/v9"
apiGroup := e.Group(apiPath)
p := pave.New()
if err := pave.EchoRegister[HealthGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/health/",
"Get API server health",
"HealthGET", func(c echo.Context) error {
var request, response = HealthGETRequest{}, HealthGETResponse{}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete HealthGET request received"
return c.JSON(response.StatusCode(), response)
}
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[ProgrammesPOSTRequest](
apiGroup,
&p,
apiPath,
http.MethodPost,
"/programme/",
"Insert programmes",
"ProgrammesPOST", func(c echo.Context) error {
var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{}
switch contentType := c.Request().Header.Get("Content-Type"); contentType {
case "application/json":
if err := c.Bind(&request.Data); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse request body: %s", err)
return c.JSON(response.StatusCode(), response)
}
case "text/csv":
body := c.Request().Body
if body == nil {
var response voki.ResponseBadRequest
response.Message = "empty request body cannot be parsed"
return c.JSON(response.StatusCode(), response)
}
defer body.Close()
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
r := csv.NewReader(in)
r.Comma = ';'
return r // Allows use ; as delimiter
})
// Parse CSV data using gocsv
if err := gocsv.Unmarshal(body, &request.Data.Programmes); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse programmes from csv: %s", err)
return c.JSON(response.StatusCode(), response)
}
default:
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("cannot parse body with content-type: %s", contentType)
return c.JSON(response.StatusCode(), response)
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete ProgrammesPOST request received"
return c.JSON(response.StatusCode(), response)
}
amountInserted, err := db.InsertProgrammes(request.Data.Programmes...)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Data.ProgrammesInserted = amountInserted
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembresPOSTRequest](
apiGroup,
&p,
apiPath,
http.MethodPost,
"/membre/",
"Insert membres",
"MembresPOST", func(c echo.Context) error {
var request, response = MembresPOSTRequest{}, MembresPOSTResponse{}
switch contentType := c.Request().Header.Get("Content-Type"); contentType {
case "application/json":
if err := c.Bind(&request.Data); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse request body: %s", err)
return c.JSON(response.StatusCode(), response)
}
case "text/csv":
body := c.Request().Body
if body == nil {
var response voki.ResponseBadRequest
response.Message = "empty request body cannot be parsed"
return c.JSON(response.StatusCode(), response)
}
defer body.Close()
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
r := csv.NewReader(in)
r.Comma = ';'
return r // Allows use ; as delimiter
})
// Parse CSV data using gocsv
if err := gocsv.Unmarshal(body, &request.Data.Membres); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse membres from csv: %s", err)
return c.JSON(response.StatusCode(), response)
}
default:
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("cannot parse body with content-type: %s", contentType)
return c.JSON(response.StatusCode(), response)
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembresPOST request received"
return c.JSON(response.StatusCode(), response)
}
amountInserted, err := db.InsertMembres(request.Data.Membres...)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Data.MembresInserted = amountInserted
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembreGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/membre/:membre_id/",
"Get membre",
"MembreGET", func(c echo.Context) error {
var request, response = MembreGETRequest{}, MembreGETResponse{}
request.Param.MembreID = c.Param("membre_id")
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembreGET request received"
return c.JSON(response.StatusCode(), response)
}
membre, err := db.GetMembre(request.Param.MembreID)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Data.Membre = membre
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembresGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/membre/",
"Get membres",
"MembresGET", func(c echo.Context) (err error) {
var request, response = MembresGETRequest{}, MembresGETResponse{}
queryLimit := c.QueryParam("limit")
if queryLimit != "" {
request.Query.Limit, err = strconv.Atoi(queryLimit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parsing limit: %s", err)
return c.JSON(response.StatusCode(), response)
}
} else {
//TODO cfg.Server.API.DefaultLimit
//TODO cfg.Client.API.Limit
request.Query.Limit = 1000
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembresGET request received"
return c.JSON(response.StatusCode(), response)
}
response.Data.Membres, err = db.GetMembres(request.Query.Limit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[ProgrammesGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/programme/",
"Get programmes",
"ProgrammesGET", func(c echo.Context) (err error) {
var request, response = ProgrammesGETRequest{}, ProgrammesGETResponse{}
queryLimit := c.QueryParam("limit")
if queryLimit != "" {
request.Query.Limit, err = strconv.Atoi(queryLimit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parsing limit: %s", err)
return c.JSON(response.StatusCode(), response)
}
} else {
//TODO cfg.API.DefaultLimit
request.Query.Limit = 1000
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete ProgrammesGET request received"
return c.JSON(response.StatusCode(), response)
}
response.Data.Programmes, err = db.GetProgrammes(request.Query.Limit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembrePreferedNamePUTRequest](
apiGroup,
&p,
apiPath,
http.MethodPut,
"/membre/:membre_id/prefered_name/",
"Update membre prefered name, which is prioritized in the membres_for_display view",
"MembrePreferedNamePUT", func(c echo.Context) error {
var request, response = MembrePreferedNamePUTRequest{}, MembrePreferedNamePUTResponse{}
request.Param.MembreID = c.Param("membre_id")
if err := c.Bind(&request.Data); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse request body: %s", err)
return c.JSON(response.StatusCode(), response)
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembrePreferedNamePUT request received"
return c.JSON(response.StatusCode(), response)
}
if err := db.UpdateMembrePreferedName(request.Param.MembreID, request.Data.PreferedName); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = fmt.Sprintf("Updated membre %s name to %s", request.Param.MembreID, request.Data.PreferedName)
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembresDisplayGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/membre/display/",
"Get membres",
"MembresDisplayGET", func(c echo.Context) (err error) {
var request, response = MembresDisplayGETRequest{}, MembresDisplayGETResponse{}
queryLimit := c.QueryParam("limit")
if queryLimit != "" {
request.Query.Limit, err = strconv.Atoi(queryLimit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parsing limit: %s", err)
return c.JSON(response.StatusCode(), response)
}
} else {
//TODO cfg.API.DefaultLimit
request.Query.Limit = 1000
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembresDisplayGET request received"
return c.JSON(response.StatusCode(), response)
}
response.Data.Membres, err = db.GetMembresForDisplay(request.Query.Limit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembreDisplayGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/membre/:membre_id/display/",
"Get membre",
"MembreDisplayGET", func(c echo.Context) error {
var request, response = MembreDisplayGETRequest{}, MembreDisplayGETResponse{}
request.Param.MembreID = c.Param("membre_id")
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembreDisplayGET request received"
return c.JSON(response.StatusCode(), response)
}
membre, err := db.GetMembreForDisplay(request.Param.MembreID)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Data.Membre = membre
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
return nil
}

14
queries/queries.go Normal file
View file

@ -0,0 +1,14 @@
package queries
import (
_ "embed"
)
//go:embed schema.sql
var sqlSchema string
//go:embed views.sql
var sqlViews string
func SQLSchema() string { return sqlSchema }
func SQLViews() string { return sqlViews }

12
queries/schema.sql Normal file
View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS programmes (
id TEXT PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS membres (
id VARCHAR(7) PRIMARY KEY,
last_name TEXT NOT NULL,
first_name TEXT NOT NULL,
prefered_name TEXT,
programme_id TEXT REFERENCES programmes(id) NOT NULL
);

23
queries/views.sql Normal file
View file

@ -0,0 +1,23 @@
-- membres_for_display affiche le numéro étudiant, nom complet OU prefered_name, et titre du programme.
--
-- Utilisé par l'application web pour rechercher et afficher les informations des membres
CREATE OR REPLACE VIEW
"membres_for_display"
AS (
SELECT
"membres".id,
CASE
WHEN
"membres".prefered_name != '' AND "membres".prefered_name IS NOT NULL
THEN
"membres".prefered_name
ELSE
CONCAT("membres".last_name, ', ', "membres".first_name)
END AS name,
"programmes".id AS programme_id,
"programmes".name AS programme_name
FROM
"membres"
INNER JOIN
"programmes" ON "programmes".id = "membres".programme_id
);

View file

@ -1,7 +0,0 @@
package responses
import "codeberg.org/vlbeaudoin/voki/response"
type GetHealthResponse struct {
response.ResponseWithError
}

View file

@ -1,13 +0,0 @@
package responses
import (
"codeberg.org/vlbeaudoin/voki/response"
"git.agecem.com/agecem/bottin/v5/models"
)
type ListMembresResponse struct {
response.ResponseWithError
Data struct {
Membres []models.Membre
}
}

View file

@ -1,17 +0,0 @@
package responses
import "codeberg.org/vlbeaudoin/voki/response"
type PostMembresResponse struct {
response.ResponseWithError
Data struct {
MembresInserted int64
}
}
type PostProgrammesResponse struct {
response.ResponseWithError
Data struct {
ProgrammesInserted int64
}
}

6
scripts/compose-inject-x509.sh Executable file
View file

@ -0,0 +1,6 @@
#!/bin/sh
docker-compose cp cert.pem api:/etc/bottin/cert.pem
docker-compose cp key.pem api:/etc/bottin/key.pem
docker-compose cp cert.pem ui:/etc/bottin/cert.pem
docker-compose cp key.pem ui:/etc/bottin/key.pem

View file

@ -0,0 +1,2 @@
#!/bin/sh
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

View file

@ -83,7 +83,7 @@ button {
</h2> </h2>
<p> <p>
Scannez la carte étudiante d'unE membre<br> Scannez la carte étudiante d'un·e membre<br>
-ou-<br> -ou-<br>
Entrez manuellement le code à 7 chiffres Entrez manuellement le code à 7 chiffres
</p> </p>
@ -100,7 +100,7 @@ button {
</ul> </ul>
</form> </form>
<p class="result">{{ .Result }}</p> <p class="result">{{ .Message }}</p>
</body> </body>
</html> </html>

27
templates/templates.go Normal file
View file

@ -0,0 +1,27 @@
package templates
import (
"embed"
"html/template"
"io"
"github.com/labstack/echo/v4"
)
//go:embed *.html
var templatesFS embed.FS
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
// NewTemplate returns a new Template instance with templates embedded from *.html
func NewTemplate() *Template {
return &Template{
templates: template.Must(template.ParseFS(templatesFS, "*.html")),
}
}

View file

@ -1,9 +0,0 @@
MIT License
Copyright (c) 2021-2023 AGECEM
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1 +0,0 @@
deprecated, see git.agecem.com/agecem/bottin or git.agecem.com/agecem/bottin/v5

View file

@ -1,10 +0,0 @@
module git.agecem.com/agecem/bottin/v4
go 1.20
//retract (
// v4.1.0
// v4.0.3
// v4.0.2
// v4.0.1
//)

View file

@ -1,10 +0,0 @@
package web
import "embed"
//go:embed templates/*
var templatesFS embed.FS
func GetTemplates() embed.FS {
return templatesFS
}

View file

@ -1,46 +0,0 @@
package webhandlers
import (
"fmt"
"net/http"
"git.agecem.com/agecem/bottin/v5/data"
"github.com/labstack/echo/v4"
)
type Handler struct {
APIClient *data.ApiClient
}
func (h *Handler) GetIndex(c echo.Context) error {
return c.Render(http.StatusOK, "index-html", nil)
}
func (h *Handler) GetMembre(c echo.Context) error {
membreID := c.QueryParam("membre_id")
membre, err := h.APIClient.GetMembre(membreID)
if err != nil {
return c.Render(http.StatusBadRequest, "index-html", struct {
Result string
}{
Result: fmt.Sprintln("👎", err.Error()),
})
}
membreResult := fmt.Sprintf(`👍
Membre trouvéE: [%s]`, membre.ID)
if membre.PreferedName != "" {
membreResult = fmt.Sprintf("%s -> %s", membreResult, membre.PreferedName)
} else {
membreResult = fmt.Sprintf("%s -> %s, %s", membreResult, membre.LastName, membre.FirstName)
}
return c.Render(http.StatusOK, "index-html", struct {
Result string
}{
Result: membreResult,
})
}