2023-03-28 02:35:42 -04:00
/ *
Copyright © 2023 AGECEM
* /
package cmd
2023-02-17 17:28:47 -05:00
import (
2023-04-21 21:34:18 -04:00
"crypto/subtle"
2023-04-26 16:51:13 -04:00
"encoding/json"
2023-03-21 20:29:06 -04:00
"fmt"
2023-04-21 21:34:18 -04:00
"log"
2023-03-28 02:35:42 -04:00
"embed"
2023-03-21 18:37:51 -04:00
"html/template"
"io"
2023-02-17 17:28:47 -05:00
"net/http"
2023-03-28 02:35:42 -04:00
"github.com/spf13/cobra"
2023-04-21 16:41:50 -04:00
"github.com/spf13/viper"
2023-03-28 02:35:42 -04:00
2023-04-26 16:51:13 -04:00
"git.agecem.com/agecem/agecem-org/api"
2023-07-04 16:05:23 -04:00
"git.agecem.com/agecem/agecem-org/config"
2023-07-04 16:47:03 -04:00
"git.agecem.com/agecem/agecem-org/media"
2023-03-21 18:37:51 -04:00
"git.agecem.com/agecem/agecem-org/public"
2023-07-24 14:29:19 -04:00
"git.agecem.com/agecem/agecem-org/serverhandlers"
2023-07-04 21:57:13 -04:00
"git.agecem.com/agecem/agecem-org/templates"
2023-02-17 17:28:47 -05:00
"github.com/labstack/echo/v4"
2023-03-21 18:37:51 -04:00
"github.com/labstack/echo/v4/middleware"
2023-02-17 17:28:47 -05:00
)
2023-03-21 18:37:51 -04:00
type Template struct {
templates * template . Template
}
2023-07-04 16:05:23 -04:00
var cfg config . Config
2023-07-04 21:57:13 -04:00
var (
publicFS embed . FS
templatesFS embed . FS
)
2023-03-21 18:37:51 -04:00
2023-03-28 02:35:42 -04:00
// serverCmd represents the server command
var serverCmd = & cobra . Command {
Use : "server" ,
Short : "Démarrer le serveur web" ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
2023-07-04 16:05:23 -04:00
if err := viper . Unmarshal ( & cfg ) ; err != nil {
log . Fatal ( err )
}
2023-07-04 20:35:13 -04:00
mediaClient , err := media . NewMediaClientFromViper ( )
if err != nil {
log . Fatal ( err )
}
new_buckets , err := mediaClient . Seed ( )
if err != nil {
log . Fatal ( err )
}
log . Printf ( "Seeded %d buckets.\n" , len ( new_buckets ) )
2023-03-28 02:35:42 -04:00
RunServer ( )
} ,
}
2023-02-23 04:24:04 -05:00
func init ( ) {
2023-03-28 02:35:42 -04:00
rootCmd . AddCommand ( serverCmd )
2023-07-04 21:57:13 -04:00
publicFS = public . GetPublicFS ( )
templatesFS = templates . GetTemplatesFS ( )
2023-04-21 16:41:50 -04:00
// server.port - --server-port
serverCmd . Flags ( ) . Int ( "server-port" , 8080 , "Port to run the webserver on (config: server.port)" )
viper . BindPFlag ( "server.port" , serverCmd . Flags ( ) . Lookup ( "server-port" ) )
2023-04-28 20:00:54 -04:00
// Not currently used
/ *
// server.documents.location - --server-documents-location
serverCmd . Flags ( ) . String ( "server-documents-location" , "us-east" , "Storage bucket location (config: server.documents.location)" )
viper . BindPFlag ( "server.documents.location" , serverCmd . Flags ( ) . Lookup ( "server-documents-location" ) )
* /
2023-04-21 16:41:50 -04:00
// server.documents.endpoint - --server-documents-endpoint
serverCmd . Flags ( ) . String ( "server-documents-endpoint" , "minio:9000" , "Storage server endpoint (config: server.documents.endpoint)" )
viper . BindPFlag ( "server.documents.endpoint" , serverCmd . Flags ( ) . Lookup ( "server-documents-endpoint" ) )
// server.documents.access_key_id - --server-documents-access-key-id
2023-07-04 20:50:41 -04:00
serverCmd . Flags ( ) . String ( "server-documents-access-key-id" , "agecem-org" , "Storage server access key id (config: server.documents.access_key_id)" )
2023-07-04 16:05:23 -04:00
viper . BindPFlag ( "server.documents.access_key_id" , serverCmd . Flags ( ) . Lookup ( "server-documents-access-key-id" ) )
2023-04-21 16:41:50 -04:00
// server.documents.secret_access_key - --server-documents-secret-access-key
2023-07-04 20:50:41 -04:00
serverCmd . Flags ( ) . String ( "server-documents-secret-access-key" , "agecem-org" , "Storage server secret access key (config: server.documents.secret_access_key)" )
2023-04-21 16:41:50 -04:00
viper . BindPFlag ( "server.documents.secret_access_key" , serverCmd . Flags ( ) . Lookup ( "server-documents-secret-access-key" ) )
// server.documents.use_ssl - --server-documents-use-ssl
2023-07-04 20:50:41 -04:00
serverCmd . Flags ( ) . Bool ( "server-documents-use-ssl" , false , "Storage server SSL status (config: server.documents.use_ssl)" )
2023-04-21 16:41:50 -04:00
viper . BindPFlag ( "server.documents.use_ssl" , serverCmd . Flags ( ) . Lookup ( "server-documents-use-ssl" ) )
// server.documents.buckets - --server-documents-buckets
2023-07-04 20:50:41 -04:00
serverCmd . Flags ( ) . StringSlice ( "server-documents-buckets" , [ ] string { "proces-verbaux" , "politiques-et-reglements" } , "Buckets that are allowed to be accessed by the API (config: server.documents.buckets)" )
2023-04-21 16:41:50 -04:00
viper . BindPFlag ( "server.documents.buckets" , serverCmd . Flags ( ) . Lookup ( "server-documents-buckets" ) )
2023-04-21 21:34:18 -04:00
// server.api.auth - --server-api-auth
2023-07-04 20:50:41 -04:00
serverCmd . Flags ( ) . Bool ( "server-api-auth" , true , "Enable to allow key authentication for /v1 routes (config: server.api.auth)" )
2023-04-21 21:34:18 -04:00
viper . BindPFlag ( "server.api.auth" , serverCmd . Flags ( ) . Lookup ( "server-api-auth" ) )
// server.api.key - --server-api-key
2023-07-04 20:50:41 -04:00
serverCmd . Flags ( ) . String ( "server-api-key" , "agecem-org" , "Key to use for authenticating to /v1 routes" )
2023-04-21 21:34:18 -04:00
viper . BindPFlag ( "server.api.key" , serverCmd . Flags ( ) . Lookup ( "server-api-key" ) )
2023-04-26 19:15:22 -04:00
2023-07-14 19:56:12 -04:00
// server.api.port
serverCmd . Flags ( ) . Int ( "server-api-port" , 8080 , "API server port (config: server.api.port)" )
viper . BindPFlag ( "server.api.port" , serverCmd . Flags ( ) . Lookup ( "server-api-port" ) )
// server.api.protocol
serverCmd . Flags ( ) . String ( "server-api-protocol" , "http" , "API server protocol (http/https) (config: server.api.protocol)" )
viper . BindPFlag ( "server.api.protocol" , serverCmd . Flags ( ) . Lookup ( "server-api-protocol" ) )
// server.api.host
serverCmd . Flags ( ) . String ( "server-api-host" , "localhost" , "API server host (config: server.api.host)" )
viper . BindPFlag ( "server.api.host" , serverCmd . Flags ( ) . Lookup ( "server-api-host" ) )
2023-04-26 19:15:22 -04:00
// server.admin.auth - --server-admin-auth
2023-07-04 20:50:41 -04:00
serverCmd . Flags ( ) . Bool ( "server-admin-auth" , true , "Enable to allow basic authentication for /admin routes (config: server.admin.auth)" )
2023-04-26 19:15:22 -04:00
viper . BindPFlag ( "server.admin.auth" , serverCmd . Flags ( ) . Lookup ( "server-admin-auth" ) )
2023-04-28 20:00:54 -04:00
// server.admin.username - --server-admin-username
2023-07-04 20:50:41 -04:00
serverCmd . Flags ( ) . String ( "server-admin-username" , "agecem-org" , "Username for basic authentication for /admin routes (config: server.admin.username)" )
2023-04-26 19:15:22 -04:00
viper . BindPFlag ( "server.admin.username" , serverCmd . Flags ( ) . Lookup ( "server-admin-username" ) )
2023-04-28 20:00:54 -04:00
// server.admin.password - --server-admin-password
2023-07-04 20:50:41 -04:00
serverCmd . Flags ( ) . String ( "server-admin-password" , "agecem-org" , "Password for basic authentication for /admin routes (config: server.admin.password)" )
2023-04-26 19:15:22 -04:00
viper . BindPFlag ( "server.admin.password" , serverCmd . Flags ( ) . Lookup ( "server-admin-password" ) )
2023-02-23 04:24:04 -05:00
}
2023-03-28 02:35:42 -04:00
func RunServer ( ) {
2023-02-17 17:28:47 -05:00
e := echo . New ( )
2023-02-23 04:24:04 -05:00
2023-03-21 18:37:51 -04:00
t := & Template {
2023-07-04 21:57:13 -04:00
templates : template . Must ( template . ParseFS ( templatesFS , "html/*.gohtml" ) ) ,
2023-03-21 18:37:51 -04:00
}
e . Renderer = t
e . Pre ( middleware . RemoveTrailingSlash ( ) )
2023-07-04 22:17:04 -04:00
groupStatic := e . Group ( "/public/*" )
groupStatic . Use ( middleware . StaticWithConfig ( middleware . StaticConfig {
Root : "/" ,
Filesystem : http . FS ( publicFS ) ,
//TODO
//Browse: true,
} ) )
2023-04-21 21:34:18 -04:00
groupV1 := e . Group ( "/v1" )
groupV1 . Use ( middleware . AddTrailingSlash ( ) )
2023-07-04 16:05:23 -04:00
if cfg . Server . Api . Auth {
if len ( cfg . Server . Api . Key ) < 10 {
2023-04-21 21:34:18 -04:00
log . Fatal ( "server.api.auth is enabled, but server.api.key is too small (needs at least 10 characters)" )
}
groupV1 . Use ( middleware . KeyAuth ( func ( key string , c echo . Context ) ( bool , error ) {
2023-07-04 16:05:23 -04:00
return subtle . ConstantTimeCompare ( [ ] byte ( key ) , [ ] byte ( cfg . Server . Api . Key ) ) == 1 , nil
2023-04-21 21:34:18 -04:00
} ) )
2023-04-26 19:15:22 -04:00
log . Println ( "Key auth for /v1 activated" )
}
groupAdmin := e . Group ( "/admin" )
groupAdmin . Use ( middleware . AddTrailingSlash ( ) )
2023-07-04 16:05:23 -04:00
if cfg . Server . Admin . Auth {
if len ( cfg . Server . Admin . Username ) < 5 {
2023-04-26 19:15:22 -04:00
log . Fatal ( "server.admin.auth is enabled, but server.admin.username is too small (needs at least 5 characters)" )
}
2023-07-04 16:05:23 -04:00
if len ( cfg . Server . Admin . Password ) < 10 {
2023-04-26 19:15:22 -04:00
log . Fatal ( "server.admin.auth is enabled, but server.admin.password is too small (needs at least 10 characters)" )
}
groupAdmin . Use ( middleware . BasicAuth ( func ( username_entered , password_entered string , c echo . Context ) ( bool , error ) {
// Be careful to use constant time comparison to prevent timing attacks
2023-07-04 16:05:23 -04:00
if subtle . ConstantTimeCompare ( [ ] byte ( username_entered ) , [ ] byte ( cfg . Server . Admin . Username ) ) == 1 &&
subtle . ConstantTimeCompare ( [ ] byte ( password_entered ) , [ ] byte ( cfg . Server . Admin . Password ) ) == 1 {
2023-04-26 19:15:22 -04:00
return true , nil
}
return false , nil
} ) )
log . Println ( "Basic auth for /admin activated" )
2023-04-21 21:34:18 -04:00
}
2023-03-21 18:48:23 -04:00
// API Routes
2023-07-24 14:29:19 -04:00
groupV1 . GET ( "" , serverhandlers . HandleV1 )
2023-04-21 21:34:18 -04:00
2023-07-24 14:29:19 -04:00
groupV1 . POST ( "/seed" , serverhandlers . HandleV1Seed )
2023-03-21 18:48:23 -04:00
2023-07-24 14:29:19 -04:00
groupV1 . GET ( "/bucket" , serverhandlers . HandleV1BucketList )
2023-04-21 21:34:18 -04:00
2023-07-24 14:29:19 -04:00
groupV1 . GET ( "/bucket/:bucket" , serverhandlers . HandleV1BucketRead )
2023-04-21 16:41:50 -04:00
2023-07-24 14:29:19 -04:00
groupV1 . POST ( "/bucket/:bucket" , serverhandlers . HandleV1DocumentCreate )
2023-04-24 17:19:52 -04:00
2023-07-24 14:29:19 -04:00
groupV1 . GET ( "/bucket/:bucket/:document" , serverhandlers . HandleV1DocumentRead )
2023-04-24 17:19:52 -04:00
2023-07-24 14:29:19 -04:00
groupV1 . PUT ( "/bucket/:bucket/:document" , serverhandlers . HandleV1DocumentUpdate )
2023-04-24 17:19:52 -04:00
2023-07-24 14:29:19 -04:00
groupV1 . DELETE ( "/bucket/:bucket/:document" , serverhandlers . HandleV1DocumentDelete )
2023-04-24 17:19:52 -04:00
2023-03-21 18:48:23 -04:00
// HTML Routes
2023-03-21 18:37:51 -04:00
e . GET ( "/" , handleIndex )
2023-08-11 22:35:26 -04:00
//e.GET("/a-propos", handleAPropos)
2023-03-21 20:29:06 -04:00
2023-08-11 22:35:26 -04:00
//e.GET("/actualite", handleActualite)
2023-03-21 20:29:06 -04:00
2023-08-11 22:35:26 -04:00
//e.GET("/actualite/:article", handleActualiteArticle)
2023-03-21 20:29:06 -04:00
e . GET ( "/vie-etudiante" , handleVieEtudiante )
e . GET ( "/vie-etudiante/:organisme" , handleVieEtudianteOrganisme )
e . GET ( "/documentation" , handleDocumentation )
2023-03-24 18:21:22 -04:00
e . GET ( "/formulaires" , handleFormulaires )
2023-03-21 20:29:06 -04:00
2023-04-26 18:27:58 -04:00
// Public Routes
e . GET ( "/public/documentation/:bucket/:document" , handlePublicDocumentation )
2023-04-26 19:15:22 -04:00
// Admin Routes
groupAdmin . GET ( "" , handleAdmin )
2023-04-26 19:28:20 -04:00
groupAdmin . GET ( "/documents/upload" , handleAdminDocumentsUpload )
2023-04-26 19:43:43 -04:00
groupAdmin . POST ( "/documents/upload" , h andleAdminDocumentsUploadPOST )
2023-04-21 16:41:50 -04:00
e . Logger . Fatal ( e . Start (
2023-07-04 16:05:23 -04:00
fmt . Sprintf ( ":%d" , cfg . Server . Port ) ) )
2023-02-17 17:28:47 -05:00
}
2023-03-21 18:37:51 -04:00
func ( t * Template ) Render ( w io . Writer , name string , data interface { } , c echo . Context ) error {
return t . templates . ExecuteTemplate ( w , name , data )
}
// HTML Handlers
func handleIndex ( c echo . Context ) error {
return c . Render ( http . StatusOK , "index-html" , nil )
}
2023-03-21 19:59:41 -04:00
2023-08-11 22:35:26 -04:00
/ *
2023-03-21 20:29:06 -04:00
func handleAPropos ( c echo . Context ) error {
2023-03-24 20:05:52 -04:00
return c . Render ( http . StatusOK , "a-propos-html" , nil )
2023-03-21 20:29:06 -04:00
}
2023-08-11 22:35:26 -04:00
* /
2023-03-21 20:29:06 -04:00
2023-08-11 22:35:26 -04:00
/ *
2023-03-21 20:29:06 -04:00
func handleActualite ( c echo . Context ) error {
2023-03-24 20:05:52 -04:00
return c . Render ( http . StatusOK , "actualite-html" , nil )
2023-03-21 20:29:06 -04:00
}
2023-08-11 22:35:26 -04:00
* /
2023-03-21 20:29:06 -04:00
2023-08-11 22:35:26 -04:00
/ *
2023-03-21 20:29:06 -04:00
func handleActualiteArticle ( c echo . Context ) error {
article := c . Param ( "article" )
return c . String ( http . StatusOK , fmt . Sprintf ( "Article: %s" , article ) )
}
2023-08-11 22:35:26 -04:00
* /
2023-04-26 15:15:43 -04:00
2023-03-21 20:29:06 -04:00
func handleVieEtudiante ( c echo . Context ) error {
2023-03-24 20:05:52 -04:00
return c . Render ( http . StatusOK , "vie-etudiante-html" , nil )
2023-03-21 20:29:06 -04:00
}
2023-04-26 15:15:43 -04:00
2023-03-21 20:29:06 -04:00
func handleVieEtudianteOrganisme ( c echo . Context ) error {
organisme := c . Param ( "organisme" )
return c . String ( http . StatusOK , fmt . Sprintf ( "Organisme: %s" , organisme ) )
}
2023-04-26 15:15:43 -04:00
2023-03-21 20:29:06 -04:00
func handleDocumentation ( c echo . Context ) error {
2023-07-14 20:14:15 -04:00
client , err := api . NewApiClientFromViper ( )
2023-04-26 16:51:13 -04:00
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 [ ] string
err = json . Unmarshal ( result , & buckets )
if err != nil {
return c . Render ( http . StatusInternalServerError , "documentation-html" , nil )
}
type Bucket struct {
Name string
Documents [ ] string
}
var data [ ] Bucket
for _ , bucket := range buckets {
2023-04-28 19:30:31 -04:00
content , err := client . Call ( http . MethodGet , fmt . Sprintf ( "/v1/bucket/%s" , bucket ) )
2023-04-26 16:51:13 -04:00
if err != nil {
return c . Render ( http . StatusInternalServerError , "documentation-html" , nil )
}
var documents [ ] string
2023-04-28 19:30:31 -04:00
err = json . Unmarshal ( content , & documents )
2023-04-26 16:51:13 -04:00
if err != nil {
return c . Render ( http . StatusInternalServerError , "documentation-html" , nil )
}
2023-04-28 19:30:31 -04:00
// 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.
/ *
2023-07-14 20:43:40 -04:00
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
2023-04-28 19:30:31 -04:00
* /
2023-04-26 16:51:13 -04:00
data = append ( data , Bucket {
Name : bucket ,
2023-07-14 20:43:40 -04:00
Documents : documents ,
2023-04-26 16:51:13 -04:00
} )
}
return c . Render ( http . StatusOK , "documentation-html" , data )
2023-03-21 20:29:06 -04:00
}
2023-04-26 15:15:43 -04:00
2023-03-24 18:21:22 -04:00
func handleFormulaires ( c echo . Context ) error {
2023-03-24 20:05:52 -04:00
return c . Render ( http . StatusOK , "formulaires-html" , nil )
2023-03-21 20:29:06 -04:00
}
2023-04-26 18:27:58 -04:00
func handlePublicDocumentation ( c echo . Context ) error {
2023-07-14 20:14:15 -04:00
client , err := api . NewApiClientFromViper ( )
2023-04-26 18:27:58 -04:00
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 )
}
2023-04-26 19:15:22 -04:00
func handleAdmin ( c echo . Context ) error {
2023-04-26 19:28:20 -04:00
return c . Render ( http . StatusOK , "admin-html" , nil )
}
func handleAdminDocumentsUpload ( c echo . Context ) error {
return c . Render ( http . StatusOK , "admin-upload-html" , nil )
2023-04-26 19:15:22 -04:00
}
2023-04-26 19:43:43 -04:00
func handleAdminDocumentsUploadPOST ( c echo . Context ) error {
2023-07-14 20:14:15 -04:00
client , err := api . New ( cfg . Server . Api . Protocol , cfg . Server . Api . Host , cfg . Server . Port , api . APIOptions {
2023-07-04 16:05:23 -04:00
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 ,
2023-04-28 15:57:09 -04:00
} )
if err != nil {
2023-04-28 17:30:08 -04:00
return c . Render ( http . StatusInternalServerError , "admin-upload-html" , struct { Message string } { Message : fmt . Sprintf ( "handleAdminDocumentsUploadPOST#api.New: %s" , err ) } )
2023-04-28 15:57:09 -04:00
}
bucket := c . FormValue ( "bucket" )
document , err := c . FormFile ( "document" )
if err != nil {
2023-04-28 17:30:08 -04:00
return c . Render ( http . StatusBadRequest , "admin-upload-html" , struct { Message string } { Message : fmt . Sprintf ( "handleAdminDocumentsUploadPOST#c.FormFile: %s" , err ) } )
2023-04-28 15:57:09 -04:00
}
response , err := client . UploadDocument ( bucket , document )
if err != nil {
2023-04-28 17:30:08 -04:00
return c . Render ( http . StatusInternalServerError , "admin-upload-html" , struct { Message string } { Message : fmt . Sprintf ( "handleAdminDocumentsUploadPOST#client.UploadDocument: %s" , err ) } )
}
// Format response
var message , info , status string
2023-04-28 19:30:31 -04:00
info = fmt . Sprintf ( "[%.0f] /public/documentation/%s/%s" , response . Info . Size , response . Info . Bucket , response . Info . Object )
status = response . Message
2023-04-26 19:43:43 -04:00
2023-04-28 17:30:08 -04:00
message = fmt . Sprintf ( "%s - %s" , status , info )
return c . Render ( http . StatusOK , "admin-upload-html" , struct { Message string } { Message : message } )
2023-04-26 19:43:43 -04:00
}