Compare commits
No commits in common. "main" and "v7.0.0" have entirely different histories.
27 changed files with 621 additions and 1015 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -22,8 +22,6 @@
|
||||||
# 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
|
||||||
|
|
12
Dockerfile
12
Dockerfile
|
@ -1,21 +1,19 @@
|
||||||
FROM golang:1.23.0 as build
|
FROM golang:1.22.3 as build
|
||||||
|
|
||||||
LABEL author="vlbeaudoin"
|
LABEL author="vlbeaudoin"
|
||||||
|
|
||||||
WORKDIR /go/src/app
|
WORKDIR /go/src/app
|
||||||
|
|
||||||
COPY go.mod go.sum LICENSE ./
|
COPY go.mod go.sum client.go client_test.go cmd.go config.go db.go entity.go main.go request.go response.go routes.go template.go ./
|
||||||
|
|
||||||
ADD cmd/ cmd/
|
ADD sql/ sql/
|
||||||
ADD pkg/ pkg/
|
|
||||||
ADD queries/ queries/
|
|
||||||
ADD templates/ templates/
|
ADD templates/ templates/
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -a -o bottin ./cmd/bottin
|
RUN CGO_ENABLED=0 go build -a -o bottin .
|
||||||
|
|
||||||
# Alpine
|
# Alpine
|
||||||
|
|
||||||
FROM alpine:3.20.2
|
FROM alpine:3.20
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
12
Makefile
12
Makefile
|
@ -14,15 +14,3 @@ help: ## Show this help
|
||||||
.PHONY: test-integration
|
.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
|
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
|
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
|
|
||||||
|
|
22
README.md
22
README.md
|
@ -20,18 +20,17 @@ 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
|
||||||
|
|
||||||
Au minimum, il faut ces 3 entrées:
|
(Remplacer `bottin` par quelque chose de plus sécuritaire)
|
||||||
|
|
||||||
*Remplacer `bottin` par quelque chose de plus sécuritaire*
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
BOTTIN_SERVER_DB_DATABASE=bottin
|
BOTTIN_API_KEY=bottin
|
||||||
BOTTIN_SERVER_DB_PASSWORD=bottin
|
BOTTIN_POSTGRES_DATABASE=bottin
|
||||||
BOTTIN_SERVER_DB_USER=bottin
|
BOTTIN_POSTGRES_PASSWORD=bottin
|
||||||
|
BOTTIN_POSTGRES_USER=bottin
|
||||||
|
BOTTIN_WEB_PASSWORD=bottin
|
||||||
|
BOTTIN_WEB_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`
|
||||||
|
@ -44,15 +43,14 @@ 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 `server.api.key` (string)*
|
*Y remplir au minimum le champs `api.key` (string)*
|
||||||
|
|
||||||
Pour modifier la configuration du client web
|
Pour modifier la configuration du client web
|
||||||
|
|
||||||
`$ docker-compose exec -it ui vi /etc/bottin/ui.yaml`
|
`$ docker-compose exec -it web vi /etc/bottin/web.yaml`
|
||||||
|
|
||||||
*Y remplir au minimum les champs `server.ui.api.key` (string), `server.ui.user` (string) et `server.ui.password` (string)*
|
*Y remplir au minimum les champs `web.api.key` (string), `web.user` (string) et `web.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
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package bottin
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
|
@ -1,12 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"codeberg.org/vlbeaudoin/voki/v3"
|
"codeberg.org/vlbeaudoin/voki/v3"
|
||||||
"git.agecem.com/agecem/bottin/v9/pkg/bottin"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,28 +13,17 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPI(t *testing.T) {
|
func TestAPI(t *testing.T) {
|
||||||
var cfg bottin.Config
|
var cfg Config
|
||||||
if err := viper.Unmarshal(&cfg); err != nil {
|
if err := viper.Unmarshal(&cfg); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//httpClient := http.DefaultClient
|
httpClient := http.DefaultClient
|
||||||
//defer httpClient.CloseIdleConnections()
|
|
||||||
|
|
||||||
transport := http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient := http.Client{
|
|
||||||
Transport: &transport,
|
|
||||||
}
|
|
||||||
defer httpClient.CloseIdleConnections()
|
defer httpClient.CloseIdleConnections()
|
||||||
|
|
||||||
vokiClient := voki.New(&httpClient, "localhost", cfg.Client.API.Key, cfg.Client.API.Port, cfg.Client.API.Protocol)
|
vokiClient := voki.New(httpClient, "localhost", cfg.API.Key, cfg.API.Port, "http")
|
||||||
apiClient := bottin.APIClient{Voki: vokiClient}
|
apiClient := APIClient{vokiClient}
|
||||||
|
|
||||||
t.Run("get API health", func(t *testing.T) {
|
t.Run("get API health", func(t *testing.T) {
|
||||||
health, err := apiClient.GetHealth()
|
health, err := apiClient.GetHealth()
|
||||||
|
@ -54,9 +41,9 @@ func TestAPI(t *testing.T) {
|
||||||
|
|
||||||
t.Run("insert programmes",
|
t.Run("insert programmes",
|
||||||
func(t *testing.T) {
|
func(t *testing.T) {
|
||||||
programmes := []bottin.Programme{
|
programmes := []Programme{
|
||||||
{ID: "404.42", Name: "Cool programme"},
|
{"404.42", "Cool programme"},
|
||||||
{ID: "200.10", Name: "Autre programme"},
|
{"200.10", "Autre programme"},
|
||||||
}
|
}
|
||||||
t.Log("programmes:", programmes)
|
t.Log("programmes:", programmes)
|
||||||
_, err := apiClient.InsertProgrammes(programmes...)
|
_, err := apiClient.InsertProgrammes(programmes...)
|
||||||
|
@ -65,7 +52,7 @@ func TestAPI(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
testMembres := []bottin.Membre{
|
testMembres := []Membre{
|
||||||
{
|
{
|
||||||
ID: "0000000",
|
ID: "0000000",
|
||||||
FirstName: "Test",
|
FirstName: "Test",
|
226
cmd.go
Normal file
226
cmd.go
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"codeberg.org/vlbeaudoin/voki/v3"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 Config
|
||||||
|
if err := viper.Unmarshal(&cfg); err != nil {
|
||||||
|
log.Fatal("parse config:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
|
||||||
|
// Middlewares
|
||||||
|
|
||||||
|
e.Pre(middleware.AddTrailingSlash())
|
||||||
|
|
||||||
|
if cfg.API.Key != "" {
|
||||||
|
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
|
||||||
|
return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.API.Key)) == 1, nil
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.DB.User,
|
||||||
|
cfg.DB.Password,
|
||||||
|
cfg.DB.Database,
|
||||||
|
cfg.DB.Host,
|
||||||
|
cfg.DB.Port,
|
||||||
|
cfg.DB.SSLMode,
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("init pgx pool:", err)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
db := &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 := addRoutes(e, db, cfg); err != nil {
|
||||||
|
log.Fatal("add routes:", err)
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
h := handlers.New(client)
|
||||||
|
|
||||||
|
e.GET("/v7/health/", h.GetHealth)
|
||||||
|
|
||||||
|
e.POST("/v7/membres/", h.PostMembres)
|
||||||
|
|
||||||
|
e.GET("/v7/membres/", h.ListMembres)
|
||||||
|
|
||||||
|
e.GET("/v7/membres/:membre_id/", h.ReadMembre)
|
||||||
|
|
||||||
|
e.PUT("/v7/membres/:membre_id/prefered_name/", h.PutMembrePreferedName)
|
||||||
|
|
||||||
|
e.POST("/v7/programmes/", h.PostProgrammes)
|
||||||
|
|
||||||
|
e.POST("/v7/seed/", h.PostSeed)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Execution
|
||||||
|
e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", cfg.API.Port)))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// Parse config
|
||||||
|
var cfg 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.Web.User)) == 1
|
||||||
|
passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Web.Password)) == 1
|
||||||
|
return usersMatch && passwordsMatch, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Templating
|
||||||
|
e.Renderer = &Template{
|
||||||
|
templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")),
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Client
|
||||||
|
apiClient := APIClient{voki.New(
|
||||||
|
http.DefaultClient,
|
||||||
|
cfg.Web.API.Host,
|
||||||
|
cfg.Web.API.Key,
|
||||||
|
cfg.Web.API.Port,
|
||||||
|
cfg.Web.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 !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
|
||||||
|
e.Logger.Fatal(e.Start(
|
||||||
|
fmt.Sprintf(":%d", cfg.Web.Port)))
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,717 +0,0 @@
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
51
compose.yaml
51
compose.yaml
|
@ -1,51 +0,0 @@
|
||||||
name: 'bottin'
|
|
||||||
services:
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: 'docker.io/library/postgres:16'
|
|
||||||
environment:
|
|
||||||
POSTGRES_DATABASE: "${BOTTIN_SERVER_API_DB_DATABASE:?}"
|
|
||||||
POSTGRES_PASSWORD: "${BOTTIN_SERVER_API_DB_PASSWORD:?}"
|
|
||||||
POSTGRES_USER: "${BOTTIN_SERVER_API_DB_USER:?}"
|
|
||||||
volumes:
|
|
||||||
- 'db-data:/var/lib/postgresql/data'
|
|
||||||
restart: 'unless-stopped'
|
|
||||||
|
|
||||||
api:
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
build: ../..
|
|
||||||
image: 'git.agecem.com/agecem/bottin:latest'
|
|
||||||
env_file: '.env'
|
|
||||||
ports:
|
|
||||||
- '1312:1312'
|
|
||||||
volumes:
|
|
||||||
- 'api-config:/etc/bottin'
|
|
||||||
restart: 'unless-stopped'
|
|
||||||
command: ['bottin', '--config', '/etc/bottin/api.yaml', 'server', 'api']
|
|
||||||
|
|
||||||
ui:
|
|
||||||
depends_on:
|
|
||||||
- api
|
|
||||||
build: ../..
|
|
||||||
image: 'git.agecem.com/agecem/bottin:latest'
|
|
||||||
env_file: '.env'
|
|
||||||
ports:
|
|
||||||
- '2312:2312'
|
|
||||||
volumes:
|
|
||||||
- 'ui-config:/etc/bottin'
|
|
||||||
restart: 'unless-stopped'
|
|
||||||
command: ['bottin', '--config', '/etc/bottin/ui.yaml', 'server', 'ui']
|
|
||||||
|
|
||||||
# adminer:
|
|
||||||
# image: adminer
|
|
||||||
# restart: always
|
|
||||||
# ports:
|
|
||||||
# - 8088:8080
|
|
||||||
# depends_on:
|
|
||||||
# - db
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
db-data:
|
|
||||||
api-config:
|
|
||||||
ui-config:
|
|
268
config.go
Normal file
268
config.go
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ViperAPIPort string = "api.port"
|
||||||
|
FlagAPIPort string = "api-port"
|
||||||
|
DefaultAPIPort int = 1312
|
||||||
|
DescriptionAPIPort string = "API server port"
|
||||||
|
|
||||||
|
ViperAPIKey string = "api.key"
|
||||||
|
FlagAPIKey string = "api-key"
|
||||||
|
DefaultAPIKey string = "bottin"
|
||||||
|
DescriptionAPIKey string = "API server key. Leave empty for no key auth (not recommended)"
|
||||||
|
|
||||||
|
ViperDBDatabase string = "db.database"
|
||||||
|
FlagDBDatabase string = "db-database"
|
||||||
|
DefaultDBDatabase string = "bottin"
|
||||||
|
DescriptionDBDatabase string = "Postgres database"
|
||||||
|
|
||||||
|
ViperDBSSLMode string = "db.sslmode"
|
||||||
|
FlagDBSSLMode string = "db-sslmode"
|
||||||
|
DefaultDBSSLMode string = "prefer"
|
||||||
|
DescriptionDBSSLMode string = "Postgres sslmode"
|
||||||
|
|
||||||
|
ViperDBHost string = "db.host"
|
||||||
|
FlagDBHost string = "db-host"
|
||||||
|
DefaultDBHost string = "db"
|
||||||
|
DescriptionDBHost string = "Postgres host"
|
||||||
|
|
||||||
|
ViperDBPassword string = "db.password"
|
||||||
|
FlagDBPassword string = "db-password"
|
||||||
|
DefaultDBPassword string = "bottin"
|
||||||
|
DescriptionDBPassword string = "Postgres password"
|
||||||
|
|
||||||
|
ViperDBPort string = "db.port"
|
||||||
|
FlagDBPort string = "db-port"
|
||||||
|
DefaultDBPort int = 5432
|
||||||
|
DescriptionDBPort string = "Postgres port"
|
||||||
|
|
||||||
|
ViperDBUser string = "db.user"
|
||||||
|
FlagDBUser string = "db-user"
|
||||||
|
DefaultDBUser string = "bottin"
|
||||||
|
DescriptionDBUser string = "Postgres user"
|
||||||
|
|
||||||
|
ViperWebUser string = "web.user"
|
||||||
|
FlagWebUser string = "web-user"
|
||||||
|
DefaultWebUser string = "bottin"
|
||||||
|
DescriptionWebUser string = "Web client basic auth user"
|
||||||
|
|
||||||
|
ViperWebPassword string = "web.password"
|
||||||
|
FlagWebPassword string = "web-password"
|
||||||
|
DefaultWebPassword string = "bottin"
|
||||||
|
DescriptionWebPassword string = "Web client basic auth password"
|
||||||
|
|
||||||
|
ViperWebPort string = "web.port"
|
||||||
|
FlagWebPort string = "web-port"
|
||||||
|
DefaultWebPort int = 2312
|
||||||
|
DescriptionWebPort string = "Web client port"
|
||||||
|
|
||||||
|
ViperWebAPIHost string = "web.api.host"
|
||||||
|
FlagWebAPIHost string = "web-api-host"
|
||||||
|
DefaultWebAPIHost string = "api"
|
||||||
|
DescriptionWebAPIHost string = "Target API server host"
|
||||||
|
|
||||||
|
ViperWebAPIKey string = "web.api.key"
|
||||||
|
FlagWebAPIKey string = "web-api-key"
|
||||||
|
DefaultWebAPIKey string = "bottin"
|
||||||
|
DescriptionWebAPIKey string = "Target API server key"
|
||||||
|
|
||||||
|
ViperWebAPIPort string = "web.api.port"
|
||||||
|
FlagWebAPIPort string = "web-api-port"
|
||||||
|
DefaultWebAPIPort int = 1312
|
||||||
|
DescriptionWebAPIPort string = "Target API server port"
|
||||||
|
|
||||||
|
ViperWebAPIProtocol string = "web.api.protocol"
|
||||||
|
FlagWebAPIProtocol string = "web-api-protocol"
|
||||||
|
DefaultWebAPIProtocol string = "http"
|
||||||
|
DescriptionWebAPIProtocol string = "Target API server protocol (http/https)"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
API struct {
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
} `yaml:"api"`
|
||||||
|
DB struct {
|
||||||
|
Database string `yaml:"database"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
SSLMode string `yaml:"sslmode"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
User string `yaml:"user"`
|
||||||
|
} `yaml:"db"`
|
||||||
|
Web struct {
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
API struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Protocol string `yaml:"protocol"`
|
||||||
|
} `yaml:"api"`
|
||||||
|
} `yaml:"web"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig returns a Config filled with the default values from the
|
||||||
|
// `Default*` constants defined in this file.
|
||||||
|
func DefaultConfig() (cfg Config) {
|
||||||
|
cfg.API.Port = DefaultAPIPort
|
||||||
|
cfg.API.Key = DefaultAPIKey
|
||||||
|
cfg.DB.Database = DefaultDBDatabase
|
||||||
|
cfg.DB.Host = DefaultDBHost
|
||||||
|
cfg.DB.SSLMode = DefaultDBSSLMode
|
||||||
|
cfg.DB.Password = DefaultDBPassword
|
||||||
|
cfg.DB.Port = DefaultDBPort
|
||||||
|
cfg.DB.User = DefaultDBUser
|
||||||
|
cfg.Web.User = DefaultWebUser
|
||||||
|
cfg.Web.Password = DefaultWebPassword
|
||||||
|
cfg.Web.Port = DefaultWebPort
|
||||||
|
cfg.Web.API.Host = DefaultWebAPIHost
|
||||||
|
cfg.Web.API.Key = DefaultWebAPIKey
|
||||||
|
cfg.Web.API.Port = DefaultWebAPIPort
|
||||||
|
cfg.Web.API.Protocol = DefaultWebAPIProtocol
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// rootCmd
|
||||||
|
|
||||||
|
cobra.OnInitialize(initConfig)
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)")
|
||||||
|
|
||||||
|
// apiCmd
|
||||||
|
|
||||||
|
rootCmd.AddCommand(apiCmd)
|
||||||
|
|
||||||
|
// api.key
|
||||||
|
apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey)
|
||||||
|
if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// api.port
|
||||||
|
apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort)
|
||||||
|
if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// db.database
|
||||||
|
apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase)
|
||||||
|
if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// db.sslmode
|
||||||
|
apiCmd.Flags().String(FlagDBSSLMode, DefaultDBSSLMode, DescriptionDBSSLMode)
|
||||||
|
if err := viper.BindPFlag(ViperDBSSLMode, apiCmd.Flags().Lookup(FlagDBSSLMode)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// db.host
|
||||||
|
apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost)
|
||||||
|
if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// db.password
|
||||||
|
apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword)
|
||||||
|
if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// db.port
|
||||||
|
apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort)
|
||||||
|
if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// db.user
|
||||||
|
apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser)
|
||||||
|
if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebCmd
|
||||||
|
rootCmd.AddCommand(webCmd)
|
||||||
|
|
||||||
|
// web.api.host
|
||||||
|
webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost)
|
||||||
|
if err := viper.BindPFlag(ViperWebAPIHost, webCmd.Flags().Lookup(FlagWebAPIHost)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// web.api.key
|
||||||
|
webCmd.Flags().String(FlagWebAPIKey, DefaultWebAPIKey, DescriptionWebAPIKey)
|
||||||
|
if err := viper.BindPFlag(ViperWebAPIKey, webCmd.Flags().Lookup(FlagWebAPIKey)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// web.api.protocol
|
||||||
|
webCmd.Flags().String(FlagWebAPIProtocol, DefaultWebAPIProtocol, DescriptionWebAPIProtocol)
|
||||||
|
if err := viper.BindPFlag(ViperWebAPIProtocol, webCmd.Flags().Lookup(FlagWebAPIProtocol)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// web.api.port
|
||||||
|
webCmd.Flags().Int(FlagWebAPIPort, DefaultWebAPIPort, DescriptionWebAPIPort)
|
||||||
|
if err := viper.BindPFlag(ViperWebAPIPort, webCmd.Flags().Lookup(FlagWebAPIPort)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// web.password
|
||||||
|
webCmd.Flags().String(FlagWebPassword, DefaultWebPassword, DescriptionWebPassword)
|
||||||
|
if err := viper.BindPFlag(ViperWebPassword, webCmd.Flags().Lookup(FlagWebPassword)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// web.port
|
||||||
|
webCmd.Flags().Int(FlagWebPort, DefaultWebPort, DescriptionWebPort)
|
||||||
|
if err := viper.BindPFlag(ViperWebPort, webCmd.Flags().Lookup(FlagWebPort)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// web.user
|
||||||
|
webCmd.Flags().String(FlagWebUser, DefaultWebUser, DescriptionWebUser)
|
||||||
|
if err := viper.BindPFlag(ViperWebUser, webCmd.Flags().Lookup(FlagWebUser)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,20 @@
|
||||||
package bottin
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.agecem.com/agecem/bottin/v9/queries"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed sql/schema.sql
|
||||||
|
var sqlSchema string
|
||||||
|
|
||||||
|
//go:embed sql/views.sql
|
||||||
|
var sqlViews string
|
||||||
|
|
||||||
type PostgresClient struct {
|
type PostgresClient struct {
|
||||||
//TODO move context out of client
|
//TODO move context out of client
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
|
@ -17,12 +22,12 @@ type PostgresClient struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *PostgresClient) CreateOrReplaceSchema() error {
|
func (db *PostgresClient) CreateOrReplaceSchema() error {
|
||||||
_, err := db.Pool.Exec(db.Ctx, queries.SQLSchema())
|
_, err := db.Pool.Exec(db.Ctx, sqlSchema)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *PostgresClient) CreateOrReplaceViews() error {
|
func (db *PostgresClient) CreateOrReplaceViews() error {
|
||||||
_, err := db.Pool.Exec(db.Ctx, queries.SQLViews())
|
_, err := db.Pool.Exec(db.Ctx, sqlViews)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
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
|
|
|
@ -1,26 +0,0 @@
|
||||||
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'
|
|
57
docker-compose.yaml
Normal file
57
docker-compose.yaml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
services:
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: 'docker.io/library/postgres:16'
|
||||||
|
environment:
|
||||||
|
POSTGRES_DATABASE: "${BOTTIN_POSTGRES_DATABASE}"
|
||||||
|
POSTGRES_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}"
|
||||||
|
POSTGRES_USER: "${BOTTIN_POSTGRES_USER}"
|
||||||
|
volumes:
|
||||||
|
- 'db-data:/var/lib/postgresql/data'
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
|
||||||
|
api:
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
build: .
|
||||||
|
image: 'git.agecem.com/agecem/bottin:latest'
|
||||||
|
environment:
|
||||||
|
BOTTIN_DB_DATABASE: "${BOTTIN_POSTGRES_DATABASE}"
|
||||||
|
BOTTIN_DB_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}"
|
||||||
|
BOTTIN_DB_USER: "${BOTTIN_POSTGRES_USER}"
|
||||||
|
BOTTIN_API_KEY: "${BOTTIN_API_KEY}"
|
||||||
|
ports:
|
||||||
|
- '1312:1312'
|
||||||
|
volumes:
|
||||||
|
- 'api-config:/etc/bottin'
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
command: ['bottin', '--config', '/etc/bottin/api.yaml', 'api']
|
||||||
|
|
||||||
|
web:
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
build: .
|
||||||
|
image: 'git.agecem.com/agecem/bottin:latest'
|
||||||
|
environment:
|
||||||
|
BOTTIN_WEB_API_KEY: "${BOTTIN_API_KEY}"
|
||||||
|
BOTTIN_WEB_PASSWORD: "${BOTTIN_WEB_PASSWORD}"
|
||||||
|
BOTTIN_WEB_USER: "${BOTTIN_WEB_USER}"
|
||||||
|
ports:
|
||||||
|
- '2312:2312'
|
||||||
|
volumes:
|
||||||
|
- 'web-config:/etc/bottin'
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
command: ['bottin', '--config', '/etc/bottin/web.yaml', 'web']
|
||||||
|
|
||||||
|
# adminer:
|
||||||
|
# image: adminer
|
||||||
|
# restart: always
|
||||||
|
# ports:
|
||||||
|
# - 8088:8080
|
||||||
|
# depends_on:
|
||||||
|
# - db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-data:
|
||||||
|
api-config:
|
||||||
|
web-config:
|
|
@ -1,4 +1,4 @@
|
||||||
package bottin
|
package main
|
||||||
|
|
||||||
import "unicode"
|
import "unicode"
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -1,4 +1,4 @@
|
||||||
module git.agecem.com/agecem/bottin/v9
|
module git.agecem.com/agecem/bottin/v7
|
||||||
|
|
||||||
go 1.22.0
|
go 1.22.0
|
||||||
|
|
||||||
|
|
18
main.go
Normal file
18
main.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO
|
||||||
|
func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error {
|
||||||
|
return fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
*/
|
|
@ -1,53 +0,0 @@
|
||||||
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"`
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
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 }
|
|
|
@ -1,4 +1,4 @@
|
||||||
package bottin
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -23,7 +23,7 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons
|
||||||
|
|
||||||
statusCode, body, err := v.CallAndParse(
|
statusCode, body, err := v.CallAndParse(
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
"/api/v9/health/",
|
"/api/v7/health/",
|
||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
@ -64,7 +64,7 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP
|
||||||
|
|
||||||
statusCode, body, err := v.CallAndParse(
|
statusCode, body, err := v.CallAndParse(
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
"/api/v9/programme/",
|
"/api/v7/programme/",
|
||||||
&buf,
|
&buf,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
@ -105,7 +105,7 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes
|
||||||
|
|
||||||
statusCode, body, err := v.CallAndParse(
|
statusCode, body, err := v.CallAndParse(
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
"/api/v9/membre/",
|
"/api/v7/membre/",
|
||||||
&buf,
|
&buf,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
@ -146,7 +146,7 @@ func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETRespons
|
||||||
|
|
||||||
statusCode, body, err := v.CallAndParse(
|
statusCode, body, err := v.CallAndParse(
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
fmt.Sprintf("/api/v9/membre/%s/", request.Param.MembreID),
|
fmt.Sprintf("/api/v7/membre/%s/", request.Param.MembreID),
|
||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
@ -180,7 +180,7 @@ func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETRespo
|
||||||
|
|
||||||
statusCode, body, err := v.CallAndParse(
|
statusCode, body, err := v.CallAndParse(
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
fmt.Sprintf("/api/v9/membre/?limit=%d", request.Query.Limit),
|
fmt.Sprintf("/api/v7/membre/?limit=%d", request.Query.Limit),
|
||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
@ -224,7 +224,7 @@ func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response Memb
|
||||||
|
|
||||||
statusCode, body, err := v.CallAndParse(
|
statusCode, body, err := v.CallAndParse(
|
||||||
http.MethodPut,
|
http.MethodPut,
|
||||||
fmt.Sprintf("/api/v9/membre/%s/prefered_name/", request.Param.MembreID),
|
fmt.Sprintf("/api/v7/membre/%s/prefered_name/", request.Param.MembreID),
|
||||||
&buf,
|
&buf,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
@ -258,7 +258,7 @@ func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGE
|
||||||
|
|
||||||
statusCode, body, err := v.CallAndParse(
|
statusCode, body, err := v.CallAndParse(
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
fmt.Sprintf("/api/v9/programme/?limit=%d", request.Query.Limit),
|
fmt.Sprintf("/api/v7/programme/?limit=%d", request.Query.Limit),
|
||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
@ -292,7 +292,7 @@ func (request MembresDisplayGETRequest) Request(v *voki.Voki) (response MembresD
|
||||||
|
|
||||||
statusCode, body, err := v.CallAndParse(
|
statusCode, body, err := v.CallAndParse(
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
fmt.Sprintf("/api/v9/membre/display/?limit=%d", request.Query.Limit),
|
fmt.Sprintf("/api/v7/membre/display/?limit=%d", request.Query.Limit),
|
||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
@ -333,7 +333,7 @@ func (request MembreDisplayGETRequest) Request(v *voki.Voki) (response MembreDis
|
||||||
|
|
||||||
statusCode, body, err := v.CallAndParse(
|
statusCode, body, err := v.CallAndParse(
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
fmt.Sprintf("/api/v9/membre/%s/display/", request.Param.MembreID),
|
fmt.Sprintf("/api/v7/membre/%s/display/", request.Param.MembreID),
|
||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
|
@ -1,4 +1,4 @@
|
||||||
package bottin
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
|
@ -1,4 +1,4 @@
|
||||||
package bottin
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
|
@ -13,11 +13,11 @@ import (
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
|
func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
|
||||||
_ = db
|
_ = db
|
||||||
_ = cfg
|
_ = cfg
|
||||||
|
|
||||||
apiPath := "/api/v9"
|
apiPath := "/api/v7"
|
||||||
apiGroup := e.Group(apiPath)
|
apiGroup := e.Group(apiPath)
|
||||||
p := pave.New()
|
p := pave.New()
|
||||||
if err := pave.EchoRegister[HealthGETRequest](
|
if err := pave.EchoRegister[HealthGETRequest](
|
||||||
|
@ -248,8 +248,7 @@ func AddRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
//TODO cfg.Server.API.DefaultLimit
|
//TODO cfg.API.DefaultLimit
|
||||||
//TODO cfg.Client.API.Limit
|
|
||||||
request.Query.Limit = 1000
|
request.Query.Limit = 1000
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
#!/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
|
|
|
@ -1,2 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
|
|
|
@ -1,4 +1,4 @@
|
||||||
package templates
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed *.html
|
//go:embed templates/*
|
||||||
var templatesFS embed.FS
|
var templatesFS embed.FS
|
||||||
|
|
||||||
type Template struct {
|
type Template struct {
|
||||||
|
@ -18,10 +18,3 @@ type Template struct {
|
||||||
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||||
return t.templates.ExecuteTemplate(w, name, data)
|
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")),
|
|
||||||
}
|
|
||||||
}
|
|
Reference in a new issue