Compare commits

..

64 commits

Author SHA1 Message Date
d08e9ffddd Merge branch 'main' into fix/affichage-admin-upload 2023-10-12 14:00:09 -04:00
d05bd9e0b6 Changement texte bouton admin.html
"Ajouter un document" plutôt que "Ajout de document"
2023-10-12 13:58:49 -04:00
48900fafaf Ajout styles et classes admin-upload
Ajout de divs pour mieux arranger le contenu
2023-10-12 13:58:01 -04:00
0f8eccae5d Merge branch 'main' of https://git.agecem.com/agecem/agecem-org 2023-10-12 11:02:59 -04:00
23bb90e91b Merge pull request 'Update docker golang -> 1.21.1' (#155) from update/docker-go-1.21.1 into main
Reviewed-on: #155
2023-10-05 19:15:48 -05:00
53ed725891 Update docker golang -> 1.21.1 2023-10-05 20:14:19 -04:00
7dbbb7b3a3 Merge pull request 'Documenter procédure de développement sans docker ou minio' (#154) from document/no-db-server into main
Reviewed-on: #154
2023-10-05 15:12:17 -05:00
d6120304b6 Documenter procédure de développement sans docker ou minio 2023-10-05 16:11:32 -04:00
9c11d1fe65 Merge pull request 'Ajouter opacité de 0.8 à snackbar' (#153) from fix/snackbar-opacity-0.8 into main
Reviewed-on: #153
2023-10-05 14:51:01 -05:00
a7ec415e90 Ajouter opacité de 0.8 à snackbar
L'opacité rend plus facile de remarquer si du contenu est situé sous la
snackbar.
2023-10-05 15:36:24 -04:00
f6d9d5f443 Merge pull request 'Permettre exécution sans base de donnée' (#152) from change/allow-running-without-db into main
Reviewed-on: #152
2023-10-05 14:32:35 -05:00
84dc500a77 Afficher message si documentation n'est pas disponible 2023-10-05 14:23:06 -04:00
76762026f3 Continuer exécution de serverCmd même sans db
Remplacer log.Fatal par log.Printf si serverCmd ne rejoint pas la db.

Autres changements:

- Clarifier messages d'erreur du *media.MediaClient de serverCmd
- Seulement seed si mediaClient est valide (should always be mais bon)
2023-10-05 14:11:48 -04:00
b2465d2324 Merge pull request 'Ajouter postes téléphoniques connus' (#147) from documentation/vie-etudiante-postes into main
Reviewed-on: #147
2023-09-11 12:03:52 -05:00
d341ca3fec Ajouter postes téléphoniques connus 2023-09-11 13:02:46 -04:00
2af20d4741 Merge pull request 'Implémenter web_handlers.WebHandler' (#146) from refactor/web-handler-dependency-injection into main
Reviewed-on: #146
2023-08-30 14:26:31 -05:00
098666289c Implémenter web_handlers.WebHandler 2023-08-30 15:24:37 -04:00
4cc879ddbc Merge pull request 'Ajouter de l'injection de dépendance à handlers API' (#145) from refactor/api-v1-handler-dependency-injection into main
Reviewed-on: #145
2023-08-30 12:47:44 -05:00
83bad16462 Ajouter de l'injection de dépendance à handlers API 2023-08-30 13:45:07 -04:00
91dba52a26 Merge pull request 'Ajouter section Contact à page d'index' (#144) from feature/index-contact into main
Reviewed-on: #144
2023-08-29 13:18:55 -05:00
47c2e50111 Ajouter section Contact à page d'index 2023-08-29 14:17:33 -04:00
2d9144f265 Merge pull request 'Ajouter description de l'AGECEM' (#142) from feature/ajout-description into main
Reviewed-on: #142
2023-08-29 12:36:37 -05:00
42b28622d7 Ajouter description de l'AGECEM 2023-08-29 13:35:07 -04:00
19ad40e104 Merge pull request 'Ajouter local ASEG à /vie-etudiante' (#141) from fix/local-aseg into main
Reviewed-on: #141
2023-08-23 15:10:36 -05:00
91e1e1efa1 Ajouter local ASEG à /vie-etudiante 2023-08-23 16:10:04 -04:00
29ad72481e Merge pull request 'Fix implémentations de UploadDocument et models.NotFoundResponse()' (#140) from fix/not-found-and-v1-document-create-response into main
Reviewed-on: #140
2023-08-23 15:03:00 -05:00
48a8c8a37a Fix implémentations de UploadDocument et models.NotFoundResponse() 2023-08-23 16:01:57 -04:00
37eb18cfa5 Merge pull request 'Ajouter et implémenter models.NotImplementedResponse()' (#139) from refactor/not-implemented-response into main
Reviewed-on: #139
2023-08-23 14:44:06 -05:00
b19238e1cc Ajouter et implémenter models.NotImplementedResponse() 2023-08-23 15:43:10 -04:00
742cf9999b Merge pull request 'Ajouter et implémenter models.V1DocumentCreateResponse' (#137) from refactor/v1-document-create-response into main
Reviewed-on: #137
2023-08-23 14:26:45 -05:00
5612b593e2 Ajouter et implémenter models.V1DocumentCreateResponse 2023-08-23 15:26:01 -04:00
89291a24ba Merge pull request 'Ajout de table dans /vie-etudiante' (#135) from feature/vie-etudiante-table into main
Reviewed-on: #135
2023-08-22 17:16:36 -05:00
39221d0001 Ajout de table dans /vie-etudiante
Présente les organismes et associations assumées présentement actives.

Doit présentement être mis à jour manuellement dans le html du projet
2023-08-22 18:12:20 -04:00
bea6c06668 Merge pull request 'Implémenter models.NotFoundResponse()' (#134) from refactor/not-found-response into main
Reviewed-on: #134
2023-08-22 14:26:56 -05:00
ba43df3e31 Implémenter NotFoundResponse() 2023-08-22 15:25:42 -04:00
83d669b7a5 Ajouter SimpleResponse et NotFoundResponse() 2023-08-22 15:24:57 -04:00
e53a879c92 Merge pull request 'Implémenter models.V1BucketReadResponse' (#133) from refactor/handle-v1-bucket-read-response into main
Reviewed-on: #133
2023-08-22 14:10:12 -05:00
a6ba62fd62 Implémenter models.V1BucketReadResponse 2023-08-22 15:08:10 -04:00
cd8c2d4955 Merge pull request 'Création et implémentation d'une snackbar' (#107) from feature/snackbar into main
Reviewed-on: #107
2023-08-22 12:59:59 -05:00
ae9c45d4db Merge branch 'main' into feature/snackbar 2023-08-22 12:59:14 -05:00
79a5c06eae Merge pull request 'Finaliser implémentation de HandleDocumentationResponse' (#132) from fix/handle-documentation-response into main
Reviewed-on: #132
2023-08-20 17:36:32 -05:00
502b57bcd5 Finaliser implémentation de HandleDocumentationResponse 2023-08-20 18:35:07 -04:00
8c4fde507b Merge pull request 'Ajouter et implémenter certaines responses' (#118) from fix/bucket_list_response into main
Reviewed-on: #118
2023-08-20 17:04:53 -05:00
3ce7ac53ca Fix web_handlers avec nouvelles responses 2023-08-20 18:03:45 -04:00
9ebf27dbaf Ajouter API.ListBuckets() 2023-08-20 17:57:36 -04:00
d736d53a43 Implémenter HandleV1BucketListResponse 2023-08-20 17:57:14 -04:00
c8af2acae9 Ajouter responses pour certaines routes 2023-08-20 17:56:49 -04:00
01317686ae Merge pull request 'Refactor api_handlers et web_handlers' (#117) from refactor/handlers into main
Reviewed-on: #117
2023-08-20 15:24:44 -05:00
3a602486df Mettre à jour Dockerfile avec refactor de packages 2023-08-20 16:21:44 -04:00
3bab5b3b51 Refactor api_handlers et web_handlers
serverhandlers -> api_handlers
html handlers dans cmd/server -> web_handlers
2023-08-20 16:19:05 -04:00
8e102e8199 Merge pull request 'Ajouter models/ pour type Bucket' (#109) from refactor/models into main
Reviewed-on: #109
2023-08-19 19:31:37 -05:00
8dd0049fba Mettre à jour utilisation de models.Bucket et V1SeedResponse 2023-08-19 19:58:09 -04:00
ab5ba6708c Ajouter models.V1SeedResponse 2023-08-19 19:57:47 -04:00
6b19aa8db6 Ajouter field Response.Error 2023-08-19 19:25:13 -04:00
8cce7414ef Déplacer UploadDocumentResponse dans models/
Refactor UploadDocumentResponse selon type models.Response

Implémenter models.Response struct et models.Responder interface
2023-08-19 15:49:57 -04:00
d4f26435e8 Remplacer deprecated ioutil.ReadAll -> io.ReadAll 2023-08-19 15:38:52 -04:00
73b5ce1bc2 Ajouter error handling à api.API#UploadDocument
Créer moins d'objets UploadDocumentResponse
2023-08-19 15:35:56 -04:00
9975d4032d Merge branch 'main' into refactor/models 2023-08-19 15:29:06 -04:00
588432e979 Merge pull request 'Standardiser l'utilisation de api#NewApiClientFromViper' (#110) from fix/use-api-client-from-viper into main
Reviewed-on: #110
2023-08-19 14:24:18 -05:00
25aaad42b6 Utiliser api.NewApiClientFromViper 2023-08-19 15:23:10 -04:00
0f60e58ec2 Ajouter models/ pour type Bucket
models.Bucket est utilisé dans cmd/server pour contenir les données
relatives à la documentation dans certaines routes html
2023-08-19 14:56:13 -04:00
a4b56214b2 Documenter NewApiClientFromViper 2023-08-19 14:45:07 -04:00
fa093f3e73 Retrait du body height: 100%; 2023-08-18 16:25:34 -04:00
d63746fb4c Création et implémentation d'une snackbar 2023-08-18 16:24:44 -04:00
18 changed files with 1112 additions and 715 deletions

View file

@ -1,4 +1,4 @@
FROM golang:1.21.0 as build FROM golang:1.21.1 as build
LABEL author="Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>" LABEL author="Victor Lacasse-Beaudoin <vlbeaudoin@agecem.org>"
@ -12,13 +12,17 @@ ADD cmd/ cmd/
ADD api/ api/ ADD api/ api/
ADD api_handlers/ api_handlers/
ADD config/ config/ ADD config/ config/
ADD media/ media/ ADD media/ media/
ADD models/ models/
ADD templates/ templates/ ADD templates/ templates/
ADD serverhandlers/ serverhandlers/ ADD web_handlers/ web_handlers/
RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o agecem-org . RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o agecem-org .

View file

@ -44,3 +44,21 @@ Voir les logs des containers
Détruire les containers Détruire les containers
`$ docker-compose down` `$ docker-compose down`
### Exemple de développement sans base de données ou docker
Pour un environnement simplifié sans minio ou docker, seul le toolchain `go` devrait être nécessaire au démarrage du serveur.
Pour une exécution sans installation permanente, veuillez utiliser:
`$ go run . server`
Si nécessaire, un fichier de config peut être déposé dans `$HOME/.agecem-org.yaml` ou spécifié tel que:
`$ go run . server --config agecem-org.yaml`
`agecem-org.yaml` doit être remplacé par le fichier de config désiré.
Pour un exemple de fichier de config en format JSON, voir le résultat de:
`go run . config`

View file

@ -6,12 +6,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"git.agecem.com/agecem/agecem-org/config" "git.agecem.com/agecem/agecem-org/config"
"git.agecem.com/agecem/agecem-org/models"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -30,17 +30,9 @@ type APIOptions struct {
Password string Password string
} }
type UploadDocumentResponse struct { // NewApiClientFromViper returns a pointer to a new API object,
Info UploadDocumentResponseInfo `json:"info"` // provided the configuration options are managed by
Message string `json:"message"` // https://git.agecem.com/agecem/agecem-org/config
}
type UploadDocumentResponseInfo struct {
Bucket string `json:"bucket"`
Object string `json:"key"`
Size float64 `json:"size"`
}
func NewApiClientFromViper() (*API, error) { func NewApiClientFromViper() (*API, error) {
var config config.Config var config config.Config
@ -107,7 +99,7 @@ func (a *API) Call(method, route string) ([]byte, error) {
defer response.Body.Close() defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body) body, err := io.ReadAll(response.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -135,7 +127,7 @@ func (a *API) Call(method, route string) ([]byte, error) {
defer resp.Body.Close() defer resp.Body.Close()
// Read Response Body // Read Response Body
respBody, err := ioutil.ReadAll(resp.Body) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -146,7 +138,8 @@ func (a *API) Call(method, route string) ([]byte, error) {
return nil, errors.New(fmt.Sprintf("method must be 'GET' or 'DELETE', got '%s'", method)) return nil, errors.New(fmt.Sprintf("method must be 'GET' or 'DELETE', got '%s'", method))
} }
func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (UploadDocumentResponse, error) { func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (models.V1DocumentCreateResponse, error) {
var response models.V1DocumentCreateResponse
endpoint := fmt.Sprintf("%s://%s:%d", endpoint := fmt.Sprintf("%s://%s:%d",
a.Protocol, a.Protocol,
a.Host, a.Host,
@ -162,34 +155,34 @@ func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (
// Add the file to the request // Add the file to the request
file, err := file_header.Open() file, err := file_header.Open()
if err != nil { if err != nil {
return UploadDocumentResponse{}, fmt.Errorf("UploadDocument#file_header.Open: %s", err) return response, fmt.Errorf("UploadDocument#file_header.Open: %s", err)
} }
defer file.Close() defer file.Close()
filename_processed, err := url.QueryUnescape(file_header.Filename) filename_processed, err := url.QueryUnescape(file_header.Filename)
if err != nil { if err != nil {
return UploadDocumentResponse{}, fmt.Errorf("UploadDocument#url.QueryUnescape: %s", err) return response, fmt.Errorf("UploadDocument#url.QueryUnescape: %s", err)
} }
part, err := writer.CreateFormFile("document", filename_processed) part, err := writer.CreateFormFile("document", filename_processed)
if err != nil { if err != nil {
return UploadDocumentResponse{}, fmt.Errorf("UploadDocument#writer.CreateFormFile: %s", err) return response, fmt.Errorf("UploadDocument#writer.CreateFormFile: %s", err)
} }
_, err = io.Copy(part, file) _, err = io.Copy(part, file)
if err != nil { if err != nil {
return UploadDocumentResponse{}, fmt.Errorf("UploadDocument#io.Copy: %s", err) return response, fmt.Errorf("UploadDocument#io.Copy: %s", err)
} }
err = writer.Close() err = writer.Close()
if err != nil { if err != nil {
return UploadDocumentResponse{}, fmt.Errorf("UploadDocument#writer.Close: %s", err) return response, fmt.Errorf("UploadDocument#writer.Close: %s", err)
} }
// Create a new HTTP request with the multipart body // Create a new HTTP request with the multipart body
req, err := http.NewRequest(http.MethodPost, current_url, body) req, err := http.NewRequest(http.MethodPost, current_url, body)
if err != nil { if err != nil {
return UploadDocumentResponse{}, fmt.Errorf("UploadDocument#http.NewRequest: %s", err) return response, fmt.Errorf("UploadDocument#http.NewRequest: %s", err)
} }
req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Content-Type", writer.FormDataContentType())
@ -202,15 +195,12 @@ func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return UploadDocumentResponse{}, fmt.Errorf("UploadDocument#client.Do: %s", err) return response, fmt.Errorf("UploadDocument#client.Do: %s", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
// Handle the response err = json.NewDecoder(resp.Body).Decode(&response)
var res UploadDocumentResponse return response, err
json.NewDecoder(resp.Body).Decode(&res)
return res, nil
} }
// CallWithData takes data and returns a string representing a response body. // CallWithData takes data and returns a string representing a response body.
@ -281,3 +271,17 @@ func (a *API) CallWithData(method, route string, data []byte) (string, error) {
//return "", errors.New(fmt.Sprintf("method must be 'POST' or 'PUT', got '%s'", method)) //return "", errors.New(fmt.Sprintf("method must be 'POST' or 'PUT', got '%s'", method))
return "", errors.New(fmt.Sprintf("method must be 'POST', got '%s'", method)) return "", errors.New(fmt.Sprintf("method must be 'POST', got '%s'", method))
} }
func (a *API) ListBuckets() (models.V1BucketListResponse, error) {
var response models.V1BucketListResponse
result, err := a.Call(http.MethodGet, "/v1/bucket")
if err != nil {
return response, err
}
if err = json.Unmarshal(result, &response); err != nil {
return response, err
}
return response, nil
}

View file

@ -0,0 +1,313 @@
package api_handlers
import (
"context"
"net/http"
"sort"
"git.agecem.com/agecem/agecem-org/config"
"git.agecem.com/agecem/agecem-org/media"
"git.agecem.com/agecem/agecem-org/models"
"github.com/labstack/echo/v4"
"github.com/minio/minio-go/v7"
)
type V1Handler struct {
Config config.Config
MediaClient *media.MediaClient
}
// API Handlers
// HandleV1 affiche les routes accessibles.
// Les routes sont triées selon .Path, pour les rendre plus facilement navigables.
func (h *V1Handler) HandleV1(c echo.Context) error {
routes := c.Echo().Routes()
sort.Slice(routes, func(i, j int) bool { return routes[i].Path < routes[j].Path })
return c.JSON(http.StatusOK, routes)
}
// HandleV1Seed créé des buckets dans minio selon la liste de buckets dans server.documents.buckets
// Les buckets sont créés avec paramètres par défaut, et sont ensuite visible dans /v1/bucket.
func (h *V1Handler) HandleV1Seed(c echo.Context) error {
var response models.V1SeedResponse
new_buckets, err := h.MediaClient.Seed()
response.Data.Buckets = new_buckets
if err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Error during mediaClient.Seed()"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
if len(new_buckets) == 0 {
response.Message = "All buckets already exist"
} else {
response.Message = "Buckets successfully created"
}
response.StatusCode = http.StatusOK
return c.JSON(response.StatusCode, response)
}
// HandleV1BucketList affiche les buckets permis par server.documents.buckets, qui existent.
func (h *V1Handler) HandleV1BucketList(c echo.Context) error {
var response models.V1BucketListResponse
var buckets = make(map[string]string)
for bucket_name, bucket_display_name := range h.Config.Server.Documents.Buckets {
exists, err := h.MediaClient.MinioClient.BucketExists(context.Background(), bucket_name)
if err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Error during minio#BucketExists"
// response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
if exists {
buckets[bucket_name] = bucket_display_name
}
}
response.StatusCode = http.StatusOK
response.Message = "Buckets list successful"
response.Data.Buckets = buckets
return c.JSON(response.StatusCode, response)
}
func (h *V1Handler) HandleV1BucketRead(c echo.Context) error {
var response models.V1BucketReadResponse
bucket := c.Param("bucket")
allowed := false
for bucket_allowed := range h.Config.Server.Documents.Buckets {
if bucket == bucket_allowed {
allowed = true
}
}
if !allowed {
return c.JSON(models.NotFoundResponse())
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
exists, err := h.MediaClient.MinioClient.BucketExists(ctx, bucket)
if err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Error during minio#BucketExists"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
if !exists {
return c.JSON(models.NotFoundResponse())
}
objectCh := h.MediaClient.MinioClient.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
for object := range objectCh {
if object.Err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Error during minio#ListObjects"
//TODO make sure this is safe
//response.Error = object.Err.Error()
return c.JSON(response.StatusCode, response)
}
response.Data.Keys = append(response.Data.Keys, object.Key)
}
response.StatusCode = http.StatusOK
response.Message = "V1BucketRead ok"
return c.JSON(response.StatusCode, response)
}
// HandleV1DocumentCreate permet d'ajouter un object dans un bucket, par multipart/form-data
func (h *V1Handler) HandleV1DocumentCreate(c echo.Context) error {
var response models.V1DocumentCreateResponse
bucket := c.Param("bucket")
form_file, err := c.FormFile("document")
if err != nil {
response.StatusCode = http.StatusBadRequest
response.Message = "Error during HandleV1DocumentCreate's echo#Context.FormFile"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
allowed := false
for bucket_allowed := range h.Config.Server.Documents.Buckets {
if bucket == bucket_allowed {
allowed = true
}
}
if !allowed {
return c.JSON(models.NotFoundResponse())
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
src, err := form_file.Open()
if err != nil {
response.StatusCode = http.StatusBadRequest
response.Message = "Error during form_file.Open()"
response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
defer src.Close()
info, err := h.MediaClient.MinioClient.PutObject(ctx, bucket, form_file.Filename, src, form_file.Size, minio.PutObjectOptions{
ContentType: form_file.Header.Get("Content-Type"),
})
if err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Error during minio#PutObject"
//response.Error = err.Error()
return c.JSON(response.StatusCode, response)
}
response.StatusCode = http.StatusOK
response.Message = "ok"
response.Data.Bucket = info.Bucket
response.Data.Key = info.Key
response.Data.Size = info.Size
return c.JSON(response.StatusCode, response)
}
// HandleV1DocumentRead permet de lire le contenu d'un fichier et protentiellement de le télécharger
func (h *V1Handler) HandleV1DocumentRead(c echo.Context) error {
bucket := c.Param("bucket")
document := c.Param("document")
allowed := false
for bucket_allowed := range h.Config.Server.Documents.Buckets {
if bucket == bucket_allowed {
allowed = true
}
}
if !allowed {
return c.JSON(models.NotFoundResponse())
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
bucket_exists, err := h.MediaClient.MinioClient.BucketExists(ctx, bucket)
if err != nil {
return c.JSON(http.StatusInternalServerError, "Error during minio#BucketExists")
}
if !bucket_exists {
return c.JSON(models.NotFoundResponse())
}
document_info, err := h.MediaClient.MinioClient.StatObject(ctx, bucket, document, minio.StatObjectOptions{})
if err != nil {
if err.Error() == "The specified key does not exist." {
return c.JSON(models.NotFoundResponse())
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
"message": "Error during minio#StatObject",
})
}
_ = document_info
document_object, err := h.MediaClient.MinioClient.GetObject(ctx, bucket, document, minio.GetObjectOptions{})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during minio#GetObject",
})
}
defer document_object.Close()
return c.Stream(http.StatusOK, document_info.ContentType, document_object)
}
// HandleV1DocumentUpdate permet de mettre à jour certains champs d'un object, comme le Content-Type ou le Filename
func (h *V1Handler) HandleV1DocumentUpdate(c echo.Context) error {
return c.JSON(models.NotImplementedResponse())
}
// HandleV1DocumentDelete permet de supprimer un object
func (h *V1Handler) HandleV1DocumentDelete(c echo.Context) error {
bucket := c.Param("bucket")
document := c.Param("document")
allowed := false
for bucket_allowed := range h.Config.Server.Documents.Buckets {
if bucket == bucket_allowed {
allowed = true
}
}
if !allowed {
return c.JSON(models.NotFoundResponse())
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
bucket_exists, err := h.MediaClient.MinioClient.BucketExists(ctx, bucket)
if err != nil {
return c.JSON(http.StatusInternalServerError, "Error during minio#BucketExists")
}
if !bucket_exists {
return c.JSON(models.NotFoundResponse())
}
document_info, err := h.MediaClient.MinioClient.StatObject(ctx, bucket, document, minio.StatObjectOptions{})
if err != nil {
if err.Error() == "The specified key does not exist." {
return c.JSON(models.NotFoundResponse())
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
"message": "Error during minio#StatObject",
})
}
//TODO Add error validation
_ = document_info
err = h.MediaClient.MinioClient.RemoveObject(ctx, bucket, document, minio.RemoveObjectOptions{})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during minio#RemoveObject",
})
}
return c.JSON(http.StatusOK, map[string]string{
"message": "Document deleted",
})
}

View file

@ -5,10 +5,8 @@ package cmd
import ( import (
"crypto/subtle" "crypto/subtle"
"encoding/json"
"fmt" "fmt"
"log" "log"
"sort"
"embed" "embed"
"html/template" "html/template"
@ -19,11 +17,12 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
"git.agecem.com/agecem/agecem-org/api" "git.agecem.com/agecem/agecem-org/api"
"git.agecem.com/agecem/agecem-org/api_handlers"
"git.agecem.com/agecem/agecem-org/config" "git.agecem.com/agecem/agecem-org/config"
"git.agecem.com/agecem/agecem-org/media" "git.agecem.com/agecem/agecem-org/media"
"git.agecem.com/agecem/agecem-org/public" "git.agecem.com/agecem/agecem-org/public"
"git.agecem.com/agecem/agecem-org/serverhandlers"
"git.agecem.com/agecem/agecem-org/templates" "git.agecem.com/agecem/agecem-org/templates"
"git.agecem.com/agecem/agecem-org/web_handlers"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
) )
@ -49,17 +48,18 @@ var serverCmd = &cobra.Command{
} }
mediaClient, err := media.NewMediaClientFromViper() mediaClient, err := media.NewMediaClientFromViper()
if err != nil { switch err != nil {
log.Fatal(err) case true:
log.Printf("media.NewMediaClientFromViper error: %s", err)
case false:
new_buckets, err := mediaClient.Seed()
if err != nil {
log.Printf("(*media.MediaClient).Seed error: %s", err)
} else {
log.Printf("Seeded %d buckets.\n", len(new_buckets))
}
} }
new_buckets, err := mediaClient.Seed()
if err != nil {
log.Fatal(err)
}
log.Printf("Seeded %d buckets.\n", len(new_buckets))
RunServer() RunServer()
}, },
} }
@ -199,52 +199,69 @@ func RunServer() {
} }
// API Routes // API Routes
mediaClient, err := media.NewMediaClientFromViper()
if err != nil {
log.Fatal("Error during NewMediaClientFromViper for API handlers")
}
groupV1.GET("", serverhandlers.HandleV1) v1Handler := api_handlers.V1Handler{
Config: cfg,
MediaClient: mediaClient,
}
groupV1.POST("/seed", serverhandlers.HandleV1Seed) groupV1.GET("", v1Handler.HandleV1)
groupV1.GET("/bucket", serverhandlers.HandleV1BucketList) groupV1.POST("/seed", v1Handler.HandleV1Seed)
groupV1.GET("/bucket/:bucket", serverhandlers.HandleV1BucketRead) groupV1.GET("/bucket", v1Handler.HandleV1BucketList)
groupV1.POST("/bucket/:bucket", serverhandlers.HandleV1DocumentCreate) groupV1.GET("/bucket/:bucket", v1Handler.HandleV1BucketRead)
groupV1.GET("/bucket/:bucket/:document", serverhandlers.HandleV1DocumentRead) groupV1.POST("/bucket/:bucket", v1Handler.HandleV1DocumentCreate)
groupV1.PUT("/bucket/:bucket/:document", serverhandlers.HandleV1DocumentUpdate) groupV1.GET("/bucket/:bucket/:document", v1Handler.HandleV1DocumentRead)
groupV1.DELETE("/bucket/:bucket/:document", serverhandlers.HandleV1DocumentDelete) groupV1.PUT("/bucket/:bucket/:document", v1Handler.HandleV1DocumentUpdate)
groupV1.DELETE("/bucket/:bucket/:document", v1Handler.HandleV1DocumentDelete)
// HTML Routes // HTML Routes
apiClient, err := api.NewApiClientFromViper()
if err != nil {
log.Fatal("Error during NewMediaClientFromViper for API handlers")
}
e.GET("/", handleIndex) webHandler := web_handlers.WebHandler{
ApiClient: apiClient,
}
//e.GET("/a-propos", handleAPropos) e.GET("/", web_handlers.HandleIndex)
//e.GET("/actualite", handleActualite) //e.GET("/a-propos", web_handlers.HandleAPropos)
//e.GET("/actualite/:article", handleActualiteArticle) //e.GET("/actualite", web_handlers.HandleActualite)
e.GET("/vie-etudiante", handleVieEtudiante) //e.GET("/actualite/:article", web_handlers.HandleActualiteArticle)
e.GET("/vie-etudiante/:organisme", handleVieEtudianteOrganisme) e.GET("/vie-etudiante", web_handlers.HandleVieEtudiante)
e.GET("/documentation", handleDocumentation) e.GET("/vie-etudiante/:organisme", web_handlers.HandleVieEtudianteOrganisme)
e.GET("/formulaires", handleFormulaires) e.GET("/documentation", webHandler.HandleDocumentation)
e.GET("/formulaires", web_handlers.HandleFormulaires)
// Public Routes // Public Routes
e.GET("/public/documentation/:bucket/:document", handlePublicDocumentation) e.GET("/public/documentation/:bucket/:document", webHandler.HandlePublicDocumentation)
// Admin Routes // Admin Routes
groupAdmin.GET("", handleAdmin) groupAdmin.GET("", web_handlers.HandleAdmin)
groupAdmin.GET("/documents/upload", handleAdminDocumentsUpload) groupAdmin.GET("/documents/upload", webHandler.HandleAdminDocumentsUpload)
groupAdmin.POST("/documents/upload", handleAdminDocumentsUploadPOST) groupAdmin.POST("/documents/upload", webHandler.HandleAdminDocumentsUploadPOST)
e.Logger.Fatal(e.Start( e.Logger.Fatal(e.Start(
fmt.Sprintf(":%d", cfg.Server.Port))) fmt.Sprintf(":%d", cfg.Server.Port)))
@ -253,248 +270,3 @@ func RunServer() {
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)
} }
// HTML Handlers
func handleIndex(c echo.Context) error {
return c.Render(http.StatusOK, "index-html", nil)
}
/*
func handleAPropos(c echo.Context) error {
return c.Render(http.StatusOK, "a-propos-html", nil)
}
*/
/*
func handleActualite(c echo.Context) error {
return c.Render(http.StatusOK, "actualite-html", nil)
}
*/
/*
func handleActualiteArticle(c echo.Context) error {
article := c.Param("article")
return c.String(http.StatusOK, fmt.Sprintf("Article: %s", article))
}
*/
func handleVieEtudiante(c echo.Context) error {
return c.Render(http.StatusOK, "vie-etudiante-html", nil)
}
func handleVieEtudianteOrganisme(c echo.Context) error {
organisme := c.Param("organisme")
return c.String(http.StatusOK, fmt.Sprintf("Organisme: %s", organisme))
}
func handleDocumentation(c echo.Context) error {
client, err := api.NewApiClientFromViper()
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
result, err := client.Call(http.MethodGet, "/v1/bucket")
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
var buckets map[string]string
err = json.Unmarshal(result, &buckets)
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
type Bucket struct {
Name string
DisplayName string
Documents []string
}
var data []Bucket
for bucket, displayName := range buckets {
content, err := client.Call(http.MethodGet, fmt.Sprintf("/v1/bucket/%s", bucket))
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
var documents []string
err = json.Unmarshal(content, &documents)
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
// Ce bloc retire tous les caractères spéciaux d'une string
// N'est pas présentement activé, car les fichiers sont processed
// à la création de toute façon.
/*
reg, err := regexp.Compile("[^.a-zA-Z0-9_-]+")
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
var documents_processed []string
for _, document := range documents {
document_processed := reg.ReplaceAllString(document, "")
documents_processed = append(documents_processed, document_processed)
}
documents_processed := documents
*/
data = append(data, Bucket{
Name: bucket,
DisplayName: displayName,
Documents: documents,
})
}
sort.SliceStable(data, func(i, j int) bool { return data[i].Name < data[j].Name })
return c.Render(http.StatusOK, "documentation-html", data)
}
func handleFormulaires(c echo.Context) error {
return c.Render(http.StatusOK, "formulaires-html", nil)
}
func handlePublicDocumentation(c echo.Context) error {
client, err := api.NewApiClientFromViper()
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
bucket := c.Param("bucket")
document := c.Param("document")
result, err := client.Call(http.MethodGet, fmt.Sprintf("/v1/bucket/%s/%s", bucket, document))
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
// Check if result can fit inside a map containing a message
var result_map map[string]string
err = json.Unmarshal(result, &result_map)
if err == nil {
return c.JSON(http.StatusBadRequest, result_map)
}
return c.Blob(http.StatusOK, "application/octet-stream", result)
}
func handleAdmin(c echo.Context) error {
return c.Render(http.StatusOK, "admin-html", nil)
}
func handleAdminDocumentsUpload(c echo.Context) error {
client, err := api.NewApiClientFromViper()
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
result, err := client.Call(http.MethodGet, "/v1/bucket")
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
var buckets map[string]string
err = json.Unmarshal(result, &buckets)
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
type Bucket struct {
Name string
DisplayName string
Documents []string
}
var data struct {
Buckets []Bucket
Message string
}
for bucketName, displayName := range buckets {
data.Buckets = append(data.Buckets, Bucket{
Name: bucketName,
DisplayName: displayName,
})
}
return c.Render(http.StatusOK, "admin-upload-html", data)
}
func handleAdminDocumentsUploadPOST(c echo.Context) error {
type Bucket struct {
Name string
DisplayName string
Documents []string
}
var data struct {
Buckets []Bucket
Message string
}
client, err := api.New(cfg.Server.Api.Protocol, cfg.Server.Api.Host, cfg.Server.Port, api.APIOptions{
KeyAuth: cfg.Server.Api.Auth,
Key: cfg.Server.Api.Key,
BasicAuth: cfg.Server.Admin.Auth,
Username: cfg.Server.Admin.Username,
Password: cfg.Server.Admin.Password,
})
if err != nil {
data.Message = fmt.Sprintf("handleAdminDocumentsUploadPOST#api.New: %s", err)
return c.Render(http.StatusInternalServerError, "admin-upload-html", data)
}
result, err := client.Call(http.MethodGet, "/v1/bucket")
if err != nil {
data.Message = "Error during GET /v1/bucket"
return c.Render(http.StatusInternalServerError, "documentation-html", data)
}
var buckets map[string]string
err = json.Unmarshal(result, &buckets)
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
for bucketName, displayName := range buckets {
data.Buckets = append(data.Buckets, Bucket{
Name: bucketName,
DisplayName: displayName,
})
}
bucket := c.FormValue("bucket")
document, err := c.FormFile("document")
if err != nil {
data.Message = fmt.Sprintf("handleAdminDocumentsUploadPOST#c.FormFile: %s", err)
return c.Render(http.StatusBadRequest, "admin-upload-html", data)
}
response, err := client.UploadDocument(bucket, document)
if err != nil {
data.Message = fmt.Sprintf("handleAdminDocumentsUploadPOST#client.UploadDocument: %s", err)
return c.Render(http.StatusInternalServerError, "admin-upload-html", data)
}
// Format response
var info, status string
info = fmt.Sprintf("[%.0f] /public/documentation/%s/%s", response.Info.Size, response.Info.Bucket, response.Info.Object)
status = response.Message
data.Message = fmt.Sprintf("%s - %s", status, info)
return c.Render(http.StatusOK, "admin-upload-html", data)
}

7
models/models.go Normal file
View file

@ -0,0 +1,7 @@
package models
type Bucket struct {
Name string
DisplayName string
Documents []string
}

92
models/responses.go Normal file
View file

@ -0,0 +1,92 @@
package models
import "net/http"
type Responder interface {
Respond() Responder
}
type Response struct {
StatusCode int `json:"status_code"`
Message string
Error string
}
func (r Response) Respond() Responder {
return r
}
type SimpleResponse struct {
Message string
}
func (r SimpleResponse) Respond() Responder {
return r
}
func NotFoundResponse() (int, SimpleResponse) {
return http.StatusNotFound, SimpleResponse{
Message: "Not Found",
}
}
func NotImplementedResponse() (int, SimpleResponse) {
return http.StatusNotImplemented, SimpleResponse{
Message: "Not Implemented",
}
}
type HandleAdminDocumentsUploadResponse struct {
Response
Data struct {
Buckets []Bucket
}
}
type HandleDocumentationResponse struct {
Response
Data struct {
Buckets []Bucket
}
}
type UploadDocumentResponse struct {
Response
Data UploadDocumentResponseData
}
type UploadDocumentResponseData struct {
Bucket string
Object string
Size float64
}
type V1SeedResponse struct {
Response
Data struct {
Buckets []string
}
}
type V1BucketListResponse struct {
Response
Data struct {
Buckets map[string]string
}
}
type V1BucketReadResponse struct {
Response
Data struct {
Keys []string
}
}
type V1DocumentCreateResponse struct {
Response
Data struct {
Bucket string
Key string
Size int64
}
}

120
public/css/admin-upload.css Normal file
View file

@ -0,0 +1,120 @@
.adminUploadForm {
font-family: 'Poppins';
display: flex;
flex-flow: column;
align-items: center;
}
.formContent {
display: flex;
flex-flow: column;
}
.formSelectDiv {
display: flex;
flex-flow: column;
margin: 20px;
}
.formLabel {
font-family: 'Poppins';
font-size: 0.875rem;
font-weight: 600;
margin: 0;
padding-top: 10px;
padding-bottom: 10px;
color: #394596;
}
.formSelect {
font-family: 'Poppins';
font-size: 0.875rem;
font-weight: 400;
border: 1px #C4C4C4 solid;
padding: 10px;
}
/*La flèche de l'élément*/
.formSelect:after {
color: #000
}
.formOption {
font-family: 'Poppins';
font-size: 0.875rem;
font-weight: 400;
margin-top: 5px;
margin-bottom: 5px;
}
.formOption:hover {
background-color: #C4C4C4;
}
.formDocUploadDiv {
display: flex;
flex-flow: column;
margin: 20px;
}
.formDocUpload {
font-family: 'Poppins';
font-size: 0.875rem;
font-weight: 500;
}
.formDocUpload::file-selector-button {
font-family: 'Poppins';
font-size: 0.875rem;
font-weight: 400;
background-color: #FF563C;
padding: 7px;
color: #fff;
border: none;
cursor: pointer;
margin-right: 15px;
}
.formSubmit {
font-family: 'Poppins';
font-size: 1rem;
font-weight: 500;
background-color: #FF563C;
padding: 10px;
color: #fff;
border: none;
cursor: pointer;
margin: 20px;
}
.confirmationMessage {
font-family: 'Poppins';
font-size: 0.75rem;
font-weight: 500;
text-align: center;
margin: 10px;
padding: 5px;
background-color: #C4C4C4;
}
@media screen and (min-width: 768px) {
.formContent {
display: flex;
flex-flow: row;
}
}
@media screen and (min-width: 1140px) {
.confirmationMessage {
font-size: 1rem;
font-weight: 500;
margin: 20px;
padding: 10px;
}
}

43
public/css/snackbar.css Normal file
View file

@ -0,0 +1,43 @@
.snackbar {
position: fixed;
bottom: 0;
background-color: #FF563C;
color: #fff;
width: 100%;
padding: 5px 0;
opacity: 0.8;
}
.snackbar-is-closed {
display: none;
}
.snackbarWrapper {
display: flex;
flex-flow: row;
align-items: center;
justify-content: space-between;
line-height: 100%;
}
span {
margin: 10px 10px 10px 0;
font-size: 0.75rem;
/*12px*/
font-family: 'Poppins';
font-weight: 600;
/*semi-bold*/
}
@media screen and (min-width: 375px) {
span {
font-size: 1rem;
/*16px*/
}
}
.snackbarFermer {
height: 30px;
width: 30px;
cursor: pointer;
}

10
public/icones/fermer.svg Normal file
View file

@ -0,0 +1,10 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1412_1060)">
<path d="M256 16C123.45 16 16 123.45 16 256C16 388.55 123.45 496 256 496C388.55 496 496 388.55 496 256C496 123.45 388.55 16 256 16ZM256 76C355.41 76 436 156.59 436 256C436 355.41 355.41 436 256 436C156.59 436 76 355.41 76 256C76 156.59 156.59 76 256 76ZM175.375 136C174.405 135.995 173.369 136.112 172.312 136.313V136.281C154.015 139.717 127.048 171.024 138.937 182.907L212.094 256.032L138.938 329.158C124.308 343.783 168.213 387.692 182.844 373.064L256 299.906L329.156 373.062C343.786 387.69 387.693 343.782 373.062 329.156L299.906 256.031L373.062 182.907C387.692 168.282 343.787 124.407 329.156 139.032L256 212.157L182.844 139.032C180.784 136.986 178.284 136.017 175.374 136.002L175.375 136Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1412_1060">
<rect width="512" height="512" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 965 B

View file

@ -1,381 +0,0 @@
package serverhandlers
import (
"context"
"net/http"
"sort"
"git.agecem.com/agecem/agecem-org/config"
"git.agecem.com/agecem/agecem-org/media"
"github.com/labstack/echo/v4"
"github.com/minio/minio-go/v7"
"github.com/spf13/viper"
)
// API Handlers
// HandleV1 affiche les routes accessibles.
// Les routes sont triées selon .Path, pour les rendre plus facilement navigables.
func HandleV1(c echo.Context) error {
routes := c.Echo().Routes()
sort.Slice(routes, func(i, j int) bool { return routes[i].Path < routes[j].Path })
return c.JSON(http.StatusOK, routes)
}
// HandleV1Seed créé des buckets dans minio selon la liste de buckets dans server.documents.buckets
// Les buckets sont créés avec paramètres par défaut, et sont ensuite visible dans /v1/bucket.
func HandleV1Seed(c echo.Context) error {
mediaClient, err := media.NewMediaClientFromViper()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during media.NewMediaClientFromViper()",
"error": err.Error(),
})
}
new_buckets, err := mediaClient.Seed()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during mediaClient.Seed()",
"error": err.Error(),
})
}
var message string
if len(new_buckets) == 0 {
message = "All buckets already exist"
} else {
message = "Buckets successfully created"
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": message,
"buckets": new_buckets,
})
}
// HandleV1BucketList affiche les buckets permis par server.documents.buckets, qui existent.
func HandleV1BucketList(c echo.Context) error {
var cfg config.Config
if err := viper.Unmarshal(&cfg); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
}
mediaClient, err := media.NewMediaClientFromViper()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during media.NewMediaClientFromViper()",
"error": err.Error(),
})
}
var buckets = make(map[string]string)
for bucket_name, bucket_display_name := range cfg.Server.Documents.Buckets {
exists, err := mediaClient.MinioClient.BucketExists(context.Background(), bucket_name)
if err != nil {
return c.JSON(http.StatusInternalServerError, "Error during minio#BucketExists")
}
if exists {
buckets[bucket_name] = bucket_display_name
}
}
return c.JSON(http.StatusOK, buckets)
}
func HandleV1BucketRead(c echo.Context) error {
var cfg config.Config
if err := viper.Unmarshal(&cfg); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
}
bucket := c.Param("bucket")
allowed := false
for bucket_allowed := range cfg.Server.Documents.Buckets {
if bucket == bucket_allowed {
allowed = true
}
}
if !allowed {
/*
return c.JSON(http.StatusBadRequest, map[string]string{
"message": "Bucket is not allowed in server.documents.buckets",
})
*/
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mediaClient, err := media.NewMediaClientFromViper()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during media.NewMediaClientFromViper()",
"error": err.Error(),
})
}
exists, err := mediaClient.MinioClient.BucketExists(ctx, bucket)
if err != nil {
return c.JSON(http.StatusInternalServerError, "Error during minio#BucketExists")
}
if !exists {
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
var keys []string
objectCh := mediaClient.MinioClient.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
for object := range objectCh {
if object.Err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during minio#ListObjects",
})
}
keys = append(keys, object.Key)
}
return c.JSON(http.StatusOK, keys)
}
// HandleV1DocumentCreate permet d'ajouter un object dans un bucket, par multipart/form-data
func HandleV1DocumentCreate(c echo.Context) error {
var cfg config.Config
if err := viper.Unmarshal(&cfg); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
}
bucket := c.Param("bucket")
form_file, err := c.FormFile("document")
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"message": "Error during HandleV1DocumentCreate's echo#Context.FormFile",
"error": err,
})
}
allowed := false
for bucket_allowed := range cfg.Server.Documents.Buckets {
if bucket == bucket_allowed {
allowed = true
}
}
if !allowed {
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mediaClient, err := media.NewMediaClientFromViper()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during media.NewMediaClientFromViper()",
"error": err.Error(),
})
}
src, err := form_file.Open()
if err != nil {
return err
}
defer src.Close()
/*
reg, err := regexp.Compile("[^.a-zA-Z0-9_-]+")
if err != nil {
return c.Render(http.StatusInternalServerError, "documentation-html", nil)
}
filename_processed := reg.ReplaceAllString(form_file.Filename, "")
*/
info, err := mediaClient.MinioClient.PutObject(ctx, bucket, form_file.Filename, src, form_file.Size, minio.PutObjectOptions{
ContentType: form_file.Header.Get("Content-Type"),
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during minio#PutObject",
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "ok",
"info": map[string]interface{}{
"bucket": info.Bucket,
"key": info.Key,
"size": info.Size,
},
})
}
// HandleV1DocumentRead permet de lire le contenu d'un fichier et protentiellement de le télécharger
func HandleV1DocumentRead(c echo.Context) error {
var cfg config.Config
if err := viper.Unmarshal(&cfg); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
}
bucket := c.Param("bucket")
document := c.Param("document")
allowed := false
for bucket_allowed := range cfg.Server.Documents.Buckets {
if bucket == bucket_allowed {
allowed = true
}
}
if !allowed {
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mediaClient, err := media.NewMediaClientFromViper()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during media.NewMediaClientFromViper()",
"error": err.Error(),
})
}
bucket_exists, err := mediaClient.MinioClient.BucketExists(ctx, bucket)
if err != nil {
return c.JSON(http.StatusInternalServerError, "Error during minio#BucketExists")
}
if !bucket_exists {
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
document_info, err := mediaClient.MinioClient.StatObject(ctx, bucket, document, minio.StatObjectOptions{})
if err != nil {
if err.Error() == "The specified key does not exist." {
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
"message": "Error during minio#StatObject",
})
}
_ = document_info
document_object, err := mediaClient.MinioClient.GetObject(ctx, bucket, document, minio.GetObjectOptions{})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during minio#GetObject",
})
}
defer document_object.Close()
return c.Stream(http.StatusOK, document_info.ContentType, document_object)
}
// HandleV1DocumentUpdate permet de mettre à jour certains champs d'un object, comme le Content-Type ou le Filename
func HandleV1DocumentUpdate(c echo.Context) error {
return c.JSON(http.StatusNotImplemented, map[string]string{
"message": "Not Implemented",
})
}
// HandleV1DocumentDelete permet de supprimer un object
func HandleV1DocumentDelete(c echo.Context) error {
var cfg config.Config
if err := viper.Unmarshal(&cfg); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
}
bucket := c.Param("bucket")
document := c.Param("document")
allowed := false
for bucket_allowed := range cfg.Server.Documents.Buckets {
if bucket == bucket_allowed {
allowed = true
}
}
if !allowed {
/*
return c.JSON(http.StatusBadRequest, map[string]string{
"message": "Bucket is not allowed in server.documents.buckets",
})
*/
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mediaClient, err := media.NewMediaClientFromViper()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during media.NewMediaClientFromViper()",
"error": err.Error(),
})
}
bucket_exists, err := mediaClient.MinioClient.BucketExists(ctx, bucket)
if err != nil {
return c.JSON(http.StatusInternalServerError, "Error during minio#BucketExists")
}
if !bucket_exists {
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
document_info, err := mediaClient.MinioClient.StatObject(ctx, bucket, document, minio.StatObjectOptions{})
if err != nil {
if err.Error() == "The specified key does not exist." {
return c.JSON(http.StatusNotFound, map[string]string{"message": "Not Found"})
}
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
"message": "Error during minio#StatObject",
})
}
//TODO Add error validation
_ = document_info
err = mediaClient.MinioClient.RemoveObject(ctx, bucket, document, minio.RemoveObjectOptions{})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error during minio#RemoveObject",
})
}
return c.JSON(http.StatusOK, map[string]string{
"message": "Document deleted",
})
}

View file

@ -5,24 +5,31 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>AGECEM</title> <title>AGECEM</title>
{{ template "general-html" }} {{ template "general-html" }}
<link rel="stylesheet" href="/public/css/admin-upload.css">
</head> </head>
<body> <body>
{{ template "header-html" }} {{ template "header-html" }}
<h1 class="heading1">Upload</h1> <div class="wrapper adminUploadWrapper">
<form class="form adminUploadForm" action="/admin/documents/upload" method="post" enctype="multipart/form-data"> <h1 class="heading1">Ajouter un document</h1>
<label class="formLabel" for="bucket">Type de document:</label> <form class="form adminUploadForm" action="/admin/documents/upload" method="post" enctype="multipart/form-data">
<select class="formSelect" name="bucket" id="bucket"> <div class="formContent">
{{ range .Buckets }} <div class="formDocUploadDiv">
<option class="formOption" value="{{ .Name }}">{{ .DisplayName }}</option> <p class="formLabel">Document à téléverser</p>
{{ end }} <input class="formDocUpload" type="file" name="document">
</select> </div>
<br> <div class="formSelectDiv">
Document: <input class="formDocUpload" type="file" name="document"> <label class="formLabel" for="bucket">Type de document</label>
<br> <select class="formSelect" name="bucket" id="bucket">
<br> {{ range .Buckets }}
<input class="formSubmit" type="submit" value="Submit"> <option class="formOption" value="{{ .Name }}">{{ .DisplayName }}</option>
</form> {{ end }}
<p>{{ .Message }}</p> </select>
</div>
</div>
<input class="formSubmit" type="submit" value="Ajouter le document">
</form>
<p class="confirmationMessage"><strong>Confirmation:</strong> {{ .Message }}</p>
</div>
</body> </body>
</html> </html>
{{ end }} {{ end }}

View file

@ -12,7 +12,7 @@
<div class="wrapper adminWrapper"> <div class="wrapper adminWrapper">
<h1 class="heading1">Admin</h1> <h1 class="heading1">Admin</h1>
<div class="adminOptions"> <div class="adminOptions">
<button class="adminOption" onclick="location.href = '/admin/documents/upload'">Ajout de document</a> <button class="adminOption" onclick="location.href = '/admin/documents/upload'">Ajouter un document</a>
</div> </div>
</div> </div>
</body> </body>

View file

@ -12,21 +12,28 @@
<div class="wrapper documentationWrapper"> <div class="wrapper documentationWrapper">
<h1 class="heading1">Documentation</h1> <h1 class="heading1">Documentation</h1>
<p> <p>
{{ range . }} {{ if not .Data.Buckets }}
{{ $bucket_name := .Name }} Documentation non-accessible pour l'instant, merci de votre patience
{{ $bucket_display_name := .DisplayName }} {{ else }}
<details class="documentationCategorie"> {{ range .Data.Buckets }}
<summary class="documentationDescription">{{ $bucket_display_name }}</summary> {{ $bucket_name := .Name }}
{{ $bucket_display_name := .DisplayName }}
<ul class="documentationListe"> <details class="documentationCategorie">
{{ range .Documents }} <summary class="documentationDescription">{{ $bucket_display_name }}</summary>
<a class ="documentationLien" href="/public/documentation/{{ $bucket_name }}/{{ . }}"><li class="documentationDocument">{{ . }}</li></a> <ul class="documentationListe">
{{ end}} {{ range .Documents }}
</ul> <a class ="documentationLien" href="/public/documentation/{{ $bucket_name }}/{{ . }}"><li class="documentationDocument">{{ . }}</li></a>
</details> {{ end}}
</ul>
</details>
{{ end }}
{{ end }} {{ end }}
</p> </p>
<p>
{{ .Message }}
</p>
</div> </div>
{{ template "snackbar-html" }}
</body> </body>
</html> </html>
{{ end }} {{ end }}

View file

@ -12,7 +12,29 @@
<div class="wrapper indexWrapper"> <div class="wrapper indexWrapper">
<h1 class="heading1">AGECEM</h1> <h1 class="heading1">AGECEM</h1>
<h2 class="heading2">Association Générale Étudiante du Cégep Édouard-Montpetit</h2> <h2 class="heading2">Association Générale Étudiante du Cégep Édouard-Montpetit</h2>
<p>
Fondée en 1976, lAssociation Étudiante est un organisme sans but lucratif voué à la défense des étudiant·e·s inscrit·e·s à lenseignement régulier du Campus de Longueuil du Cégep Édouard-Montpetit, quiels étudient de jour ou de soir, à temps plein ou à temps partiel.
</p>
<p>
Forte denviron 6000 membres, elle veille à promouvoir un milieu pédagogique sain en se consacrant à les représenter, tant au niveau académique, politique, social, quenvironnemental.
</p>
<br>
<h2 class="heading2">Contact</h2>
<h3>Courriel</h3>
permanence@agecem.org
<h3>Téléphone</h3>
(450) 679-7375
<h3>Addresse de coordination</h3>
945 Chemin de Chambly, Longueuil, QC J4H 3M6
<h3>Local</h3>
B-31
<h3>Réseaux sociaux</h3>
<ul>
<li><a href="https://www.facebook.com/asso.agecem">Facebook</a></li>
<li><a href="https://www.instagram.com/agecem_officiel"/>Instagram</a></li>
</ul>
</div> </div>
{{ template "snackbar-html" }}
</body> </body>
</html> </html>
{{ end }} {{ end }}

View file

@ -0,0 +1,15 @@
{{ define "snackbar-html" }}
<link rel="stylesheet" href="/public/css/snackbar.css">
<script>
function closeSnackbar() {
var snackbar = document.querySelector(".snackbar");
snackbar.classList.add('snackbar-is-closed');
}
</script>
<div class="snackbar">
<div class="wrapper snackbarWrapper">
<span class="snackbarTexte">Ce site web est présentement en construction.</span>
<img src="/public/icones/fermer.svg" class="snackbarFermer" onclick="closeSnackbar()"></img>
</div>
</div>
{{ end }}

View file

@ -10,7 +10,147 @@
{{ template "header-html" }} {{ template "header-html" }}
<div class="wrapper vieEtudianteWrapper"> <div class="wrapper vieEtudianteWrapper">
<h1 class="heading1">Vie étudiante</h1> <h1 class="heading1">Vie étudiante</h1>
<h3>Organismes thématiques</h3>
<table>
<tr>
<td>Nom</td>
<td>Local</td>
<td>Poste téléphonique</td>
</tr>
<tr>
<td>AME</td>
<td>C-060</td>
<td>7919</td>
</tr>
<tr>
<td>BEAM</td>
<td>F-024a</td>
<td>5930</td>
</tr>
<tr>
<td>CIC</td>
<td>F-027c</td>
</tr>
<tr>
<td>Club Aventurier</td>
<td>F-011b</td>
<td>2730</td>
</tr>
<tr>
<td>Équipe Santé</td>
<td>F-011</td>
<td>2361</td>
</tr>
<tr>
<td>Montpetit Donjon</td>
<td>C-067</td>
<td>2299</td>
</tr>
<tr>
<td>MAEL</td>
<td>F-027b</td>
</tr>
<tr>
<td>OGRE</td>
<td>F-011c</td>
<td>5647</td>
</tr>
<tr>
<td>Radio</td>
</tr>
<tr>
<td>ORGASME</td>
<td>F-027d</td>
</tr>
<tr>
<td>SOI</td>
</tr>
<tr>
<td>MotDit</td>
</tr>
</table>
<hr>
<h3>Associations de programme</h3>
<table>
<tr>
<td>Nom</td>
<td>Local</td>
<td>Poste téléphonique</td>
</tr>
<tr>
<td>ADEPT</td>
<td>F-045</td>
<td>2286</td>
</tr>
<tr>
<td>ASI</td>
</tr>
<tr>
<td>ATIM</td>
<td>F-041</td>
<td>2652</td>
</tr>
<tr>
<td>AEALC</td>
<td>A-125r</td>
<td>2873</td>
</tr>
<tr>
<td>PAPI</td>
<td>F-023</td>
<td>2795</td>
</tr>
<tr>
<td>TEE</td>
</tr>
<tr>
<td>TGE</td>
<td>C-063</td>
<td>2638</td>
</tr>
</table>
<hr>
<h3>Comités</h3>
<table>
<tr>
<td>Nom</td>
<td>Local</td>
<td>Poste téléphonique</td>
</tr>
<tr>
<td>CAP</td>
</tr>
<tr>
<td>ESPACE</td>
<td>F-011d</td>
<td>2418</td>
</tr>
<tr>
<td>CFEM</td>
</tr>
<tr>
<td>ASEG</td>
<td>B-06</td>
</tr>
<tr>
<td>Comité Mob</td>
</tr>
<tr>
<td>EUMC-CEM</td>
<td>C-054</td>
<td>2356</td>
</tr>
<tr>
<td>CÉSI</td>
</tr>
<tr>
<td>Friperie</td>
<td>F-027a</td>
<td>2248</td>
</tr>
</table>
</div> </div>
{{ template "snackbar-html" }}
</body> </body>
</html> </html>
{{ end }} {{ end }}

View file

@ -0,0 +1,204 @@
package web_handlers
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"git.agecem.com/agecem/agecem-org/api"
"git.agecem.com/agecem/agecem-org/models"
"github.com/labstack/echo/v4"
)
type WebHandler struct {
ApiClient *api.API
}
func HandleIndex(c echo.Context) error {
return c.Render(http.StatusOK, "index-html", nil)
}
/*
func HandleAPropos(c echo.Context) error {
return c.Render(http.StatusOK, "a-propos-html", nil)
}
*/
/*
func HandleActualite(c echo.Context) error {
return c.Render(http.StatusOK, "actualite-html", nil)
}
*/
/*
func HandleActualiteArticle(c echo.Context) error {
article := c.Param("article")
return c.String(http.StatusOK, fmt.Sprintf("Article: %s", article))
}
*/
func HandleVieEtudiante(c echo.Context) error {
return c.Render(http.StatusOK, "vie-etudiante-html", nil)
}
func HandleVieEtudianteOrganisme(c echo.Context) error {
organisme := c.Param("organisme")
return c.String(http.StatusOK, fmt.Sprintf("Organisme: %s", organisme))
}
func (h *WebHandler) HandleDocumentation(c echo.Context) error {
var response models.HandleDocumentationResponse
v1BucketListResponse, err := h.ApiClient.ListBuckets()
if err != nil {
response.StatusCode = v1BucketListResponse.StatusCode
response.Message = v1BucketListResponse.Message
response.Error = err.Error()
return c.Render(response.StatusCode, "documentation-html", response)
}
//TODO check v1BucketListRespone StatusCode and Error
for bucket, displayName := range v1BucketListResponse.Data.Buckets {
// TODO move call to dedicated API client method
content, err := h.ApiClient.Call(http.MethodGet, fmt.Sprintf("/v1/bucket/%s", bucket))
if err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Error during /v1/bucket/:bucket"
response.Error = err.Error()
return c.Render(response.StatusCode, "documentation-html", response)
}
var v1BucketReadResponse models.V1BucketReadResponse
err = json.Unmarshal(content, &v1BucketReadResponse)
if err != nil {
response.StatusCode = http.StatusInternalServerError
response.Message = "Error during json.Unmarshal /v1/bucket/:bucket"
response.Error = err.Error()
return c.Render(response.StatusCode, "documentation-html", response)
}
response.Data.Buckets = append(response.Data.Buckets, models.Bucket{
Name: bucket,
DisplayName: displayName,
Documents: v1BucketReadResponse.Data.Keys,
})
}
sort.SliceStable(response.Data.Buckets, func(i, j int) bool { return response.Data.Buckets[i].Name < response.Data.Buckets[j].Name })
response.StatusCode = http.StatusOK
//response.Message = "HandleDocumentation ok"
// TODO render .Message
return c.Render(response.StatusCode, "documentation-html", response)
//return c.Render(response.StatusCode, "documentation-html", response.Data.Buckets)
}
func HandleFormulaires(c echo.Context) error {
return c.Render(http.StatusOK, "formulaires-html", nil)
}
func (h *WebHandler) HandlePublicDocumentation(c echo.Context) error {
bucket := c.Param("bucket")
document := c.Param("document")
result, err := h.ApiClient.Call(http.MethodGet, fmt.Sprintf("/v1/bucket/%s/%s", bucket, document))
if err != nil {
return c.JSON(models.NotFoundResponse())
}
// Check if result can fit inside a map containing a message
var result_map map[string]string
err = json.Unmarshal(result, &result_map)
if err == nil {
return c.JSON(http.StatusBadRequest, result_map)
}
return c.Blob(http.StatusOK, "application/octet-stream", result)
}
func HandleAdmin(c echo.Context) error {
return c.Render(http.StatusOK, "admin-html", nil)
}
func (h *WebHandler) HandleAdminDocumentsUpload(c echo.Context) error {
var response models.HandleAdminDocumentsUploadResponse
v1BucketListResponse, err := h.ApiClient.ListBuckets()
if err != nil {
response.StatusCode = v1BucketListResponse.StatusCode
response.Error = err.Error()
response.Message = v1BucketListResponse.Message
return c.Render(response.StatusCode, "admin-upload-html", nil)
}
for bucketName, displayName := range v1BucketListResponse.Data.Buckets {
response.Data.Buckets = append(response.Data.Buckets, models.Bucket{
Name: bucketName,
DisplayName: displayName,
})
}
response.StatusCode = http.StatusOK
return c.Render(response.StatusCode, "admin-upload-html", response)
}
func (h *WebHandler) HandleAdminDocumentsUploadPOST(c echo.Context) error {
var response models.HandleAdminDocumentsUploadResponse
v1BucketListResponse, err := h.ApiClient.ListBuckets()
if err != nil {
response.StatusCode = v1BucketListResponse.StatusCode
response.Message = v1BucketListResponse.Message
response.Error = err.Error()
return c.Render(response.StatusCode, "admin-upload-html", response)
}
for bucketName, displayName := range v1BucketListResponse.Data.Buckets {
response.Data.Buckets = append(response.Data.Buckets, models.Bucket{
Name: bucketName,
DisplayName: displayName,
})
}
bucket := c.FormValue("bucket")
document, err := c.FormFile("document")
if err != nil {
response.StatusCode = http.StatusBadRequest
response.Message = "Formulaire invalide"
response.Error = err.Error()
return c.Render(response.StatusCode, "admin-upload-html", response)
}
uploadDocumentResponse, err := h.ApiClient.UploadDocument(bucket, document)
if err != nil {
response.StatusCode = uploadDocumentResponse.StatusCode
response.Message = uploadDocumentResponse.Message
response.Error = err.Error()
return c.Render(response.StatusCode, "admin-upload-html", response)
}
// Format response
var info, status string
info = fmt.Sprintf("[%d] /public/documentation/%s/%s", uploadDocumentResponse.Data.Size, uploadDocumentResponse.Data.Bucket, uploadDocumentResponse.Data.Key)
status = uploadDocumentResponse.Message
response.StatusCode = http.StatusOK
response.Message = fmt.Sprintf("%s - %s", status, info)
return c.Render(response.StatusCode, "admin-upload-html", response)
}