2023-03-28 02:35:42 -04:00
/ *
2024-08-22 13:22:11 -04:00
Copyright © 2023 - 2024 AGECEM
2023-03-28 02:35:42 -04:00
* /
2024-08-22 13:22:11 -04:00
package main
2023-02-17 17:28:47 -05:00
import (
2023-04-21 21:34:18 -04:00
"crypto/subtle"
2023-03-28 02:35:42 -04:00
"embed"
2024-08-22 13:22:11 -04:00
"encoding/json"
"fmt"
2023-03-21 18:37:51 -04:00
"io"
2024-08-22 13:22:11 -04:00
"log"
2023-02-17 17:28:47 -05:00
"net/http"
2024-08-22 13:22:11 -04:00
"os"
"strings"
"text/template"
2023-02-17 17:28:47 -05:00
2024-08-21 13:39:03 -04:00
"codeberg.org/vlbeaudoin/pave/v2"
2023-10-17 17:30:37 -04:00
"codeberg.org/vlbeaudoin/serpents"
2023-03-21 18:37:51 -04:00
"git.agecem.com/agecem/agecem-org/public"
2023-07-04 21:57:13 -04:00
"git.agecem.com/agecem/agecem-org/templates"
2024-08-22 13:22:11 -04:00
"git.agecem.com/agecem/agecem-org/version"
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"
2024-08-22 13:22:11 -04:00
"github.com/spf13/cobra"
"github.com/spf13/viper"
2023-02-17 17:28:47 -05:00
)
2024-08-22 13:22:11 -04:00
// configCmd represents the config command
var configCmd = & cobra . Command {
Use : "config" ,
Short : "Print the config to stdout in indented JSON format" ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
var cfg Config
if err := viper . Unmarshal ( & cfg ) ; err != nil {
log . Fatal ( err )
}
printConfig ( cfg )
} ,
}
func init ( ) {
rootCmd . AddCommand ( configCmd )
}
func printConfig ( config Config ) error {
buf , err := json . MarshalIndent ( config , "" , " " )
if err != nil {
return err
}
fmt . Println ( string ( buf ) )
return nil
}
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = & cobra . Command {
Use : "agecem-org" ,
Short : "Application du site web de l'AGECEM" ,
Long : "Application du site web de l'AGECEM, l'Association Générale Étudiante du Cégep Édouard-Montpetit." ,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute ( ) {
err := rootCmd . Execute ( )
if err != nil {
os . Exit ( 1 )
}
}
func init ( ) {
cobra . OnInitialize ( initConfig )
rootCmd . PersistentFlags ( ) . StringVar ( & cfgFile , "config" , "" , "config file (default is $HOME/.agecem-org.yaml)" )
}
// initConfig reads in config file and ENV variables if set.
func initConfig ( ) {
if cfgFile != "" {
// Use config file from the flag.
viper . SetConfigFile ( cfgFile )
} else {
// Find home directory.
home , err := os . UserHomeDir ( )
cobra . CheckErr ( err )
// Search config in home directory with name ".agecem-org" (without extension).
viper . AddConfigPath ( home )
viper . SetConfigType ( "yaml" )
viper . SetConfigName ( ".agecem-org" )
}
viper . SetEnvPrefix ( "AGECEM_ORG" )
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 ( ) )
}
}
2023-03-21 18:37:51 -04:00
type Template struct {
templates * template . Template
}
2024-08-22 13:22:11 -04:00
var cfg Config
2023-07-04 16:05:23 -04:00
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 )
}
2024-08-22 13:33:15 -04:00
mediaClient , err := NewMediaClientFromViper ( )
2023-10-05 14:11:48 -04:00
switch err != nil {
case true :
2024-08-22 13:33:15 -04:00
log . Printf ( "NewMediaClientFromViper error: %s" , err )
2023-10-05 14:11:48 -04:00
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 ) )
}
2023-07-04 20:35:13 -04:00
}
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
2023-10-17 17:30:37 -04:00
serpents . Int ( serverCmd . Flags ( ) ,
"server.port" , "server-port" , 8080 ,
"Port to run the webserver on" )
2023-04-21 16:41:50 -04:00
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
2023-10-17 17:30:37 -04:00
serpents . String ( serverCmd . Flags ( ) ,
"server.documents.endpoint" , "server-documents-endpoint" , "minio:9000" ,
"Storage server endpoint" )
serpents . String ( serverCmd . Flags ( ) ,
"server.documents.access_key_id" , "server-documents-access-key-id" , "agecem-org" ,
"Storage server access key id" )
serpents . String ( serverCmd . Flags ( ) ,
"server.documents.secret_access_key" , "server-documents-secret-access-key" , "agecem-org" ,
"Storage server secret access key" )
serpents . Bool ( serverCmd . Flags ( ) ,
"server.documents.use_ssl" , "server-documents-use-ssl" , false ,
"Storage server SSL status" )
serpents . StringToString ( serverCmd . Flags ( ) ,
"server.documents.buckets" , "server-documents-buckets" , map [ string ] string {
"proces-verbaux" : "Procès-verbaux" ,
"politiques" : "Politiques" ,
"reglements" : "Règlements" ,
"formulaires" : "Formulaires" ,
} ,
"Buckets that are allowed to be accessed by the API" )
serpents . Bool ( serverCmd . Flags ( ) ,
"server.api.auth" , "server-api-auth" , true ,
"Enable to allow key authentication for /v1 routes" )
serpents . String ( serverCmd . Flags ( ) ,
"server.api.key" , "server-api-key" , "agecem-org" ,
"Key to use for authenticating to /v1 routes" )
serpents . Int ( serverCmd . Flags ( ) ,
"server.api.port" , "server-api-port" , 8080 ,
"API server port" )
serpents . String ( serverCmd . Flags ( ) ,
"server.api.protocol" , "server-api-protocol" , "http" ,
"API server protocol (http/https)" )
serpents . String ( serverCmd . Flags ( ) ,
"server.api.host" , "server-api-host" , "localhost" ,
"API server host" )
serpents . Bool ( serverCmd . Flags ( ) ,
"server.admin.auth" , "server-admin-auth" , true ,
"Enable to allow basic authentication for /admin routes" )
serpents . String ( serverCmd . Flags ( ) ,
"server.admin.username" , "server-admin-username" , "agecem-org" ,
"Username for basic authentication for /admin routes" )
serpents . String ( serverCmd . Flags ( ) ,
"server.admin.password" , "server-admin-password" , "agecem-org" ,
"Password for basic authentication for /admin routes" )
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-12-12 17:34:43 -05:00
templates : template . Must ( template . ParseFS ( templatesFS , "html/*.html" ) ) ,
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
2024-08-22 13:33:15 -04:00
mediaClient , err := NewMediaClientFromViper ( )
2023-08-30 13:45:07 -04:00
if err != nil {
log . Fatal ( "Error during NewMediaClientFromViper for API handlers" )
}
2023-11-20 15:13:42 -05:00
p := pave . New ( )
2024-08-22 13:22:11 -04:00
v1Handler := V1Handler {
2023-08-30 13:45:07 -04:00
Config : cfg ,
MediaClient : mediaClient ,
2023-11-20 15:13:42 -05:00
Pave : & p ,
2023-08-30 13:45:07 -04:00
}
2023-03-21 18:48:23 -04:00
2024-08-22 13:22:11 -04:00
groupV1 . GET ( "" , v1Handler . ListRoutes )
2023-04-21 21:34:18 -04:00
2023-11-20 15:13:42 -05:00
if err := pave . EchoRegister [
2024-08-22 13:22:11 -04:00
ExecuteSeedRequest ,
ExecuteSeedResponse ] ( groupV1 , & p , "/v1" , http . MethodPost , "/seed" , "Créer buckets manquants définis dans `server.documents.buckets`" , "ExecuteSeed" , v1Handler . ExecuteSeed ) ; err != nil {
2023-11-20 15:13:42 -05:00
log . Fatal ( err )
}
2024-01-23 16:50:03 -05:00
if err := pave . EchoRegister [
2024-08-22 13:22:11 -04:00
UpdateDocumentKeyRequest ,
UpdateDocumentKeyResponse ] ( groupV1 , & p , "/v1" , http . MethodPut , "/bucket/:bucket/:document/key" , "Renommer un document" , "UpdateDocumentKey" , v1Handler . UpdateDocumentKey ) ; err != nil {
2024-01-23 16:50:03 -05:00
log . Fatal ( err )
}
2023-11-20 15:13:42 -05:00
if err := pave . EchoRegister [
2024-08-22 13:22:11 -04:00
ReadSpecRequest ,
ReadSpecResponse ] ( groupV1 , & p , "/v1" , http . MethodGet , "/spec" , DescriptionV1SpecGET , "SpecRead" , v1Handler . ReadSpec ) ; err != nil {
2023-11-20 15:13:42 -05:00
log . Fatal ( err )
}
2023-03-21 18:48:23 -04:00
2023-11-20 16:14:22 -05:00
if err := pave . EchoRegister [
2024-08-22 13:22:11 -04:00
ListBucketsRequest ,
ListBucketsResponse ] ( groupV1 , & p , "/v1" , http . MethodGet , "/bucket" , "List buckets" , "ListBuckets" , v1Handler . ListBuckets ) ; err != nil {
2023-11-20 16:14:22 -05:00
log . Fatal ( err )
}
2023-04-21 21:34:18 -04:00
2023-11-20 16:14:22 -05:00
if err := pave . EchoRegister [
2024-08-22 13:22:11 -04:00
ReadBucketRequest ,
ReadBucketResponse ] ( groupV1 , & p , "/v1" , http . MethodGet , "/bucket/:bucket" , "Read bucket content" , "ReadBucket" , v1Handler . ReadBucket ) ; err != nil {
2023-11-20 16:14:22 -05:00
log . Fatal ( err )
}
2023-04-21 16:41:50 -04:00
2023-12-18 17:46:31 -05:00
if err := pave . EchoRegister [
2024-08-22 13:22:11 -04:00
CreateDocumentsRequest ,
CreateDocumentsResponse ] ( groupV1 , & p , "/v1" , http . MethodPost , "/bucket/:bucket/many" , "Upload documents to specified bucket" , "CreateDocuments" , v1Handler . CreateDocuments ) ; err != nil {
2023-12-18 17:46:31 -05:00
log . Fatal ( err )
}
2023-11-20 16:34:19 -05:00
if err := pave . EchoRegister [
2024-08-22 13:22:11 -04:00
CreateDocumentRequest ,
CreateDocumentResponse ] ( groupV1 , & p , "/v1" , http . MethodPost , "/bucket/:bucket" , "Upload document to specified bucket" , "CreateDocument" , v1Handler . CreateDocument ) ; err != nil {
2023-11-20 16:34:19 -05:00
log . Fatal ( err )
}
2023-04-24 17:19:52 -04:00
2024-08-22 13:22:11 -04:00
// Do not move to pave, uses echo.Stream instead of echo.JSON
groupV1 . GET ( "/bucket/:bucket/:document" , v1Handler . ReadDocument )
2023-04-24 17:19:52 -04:00
2023-11-20 16:56:44 -05:00
if err := pave . EchoRegister [
2024-08-22 13:22:11 -04:00
DeleteDocumentRequest ,
DeleteDocumentResponse ] ( groupV1 , & p , "/v1" , http . MethodDelete , "/bucket/:bucket/:document" , "Delete document in specified bucket" , "DeleteDocument" , v1Handler . DeleteDocument ) ; err != nil {
2023-11-20 16:56:44 -05:00
log . Fatal ( err )
}
2023-04-24 17:19:52 -04:00
2023-03-21 18:48:23 -04:00
// HTML Routes
2023-10-24 17:00:49 -04:00
client := http . DefaultClient
defer client . CloseIdleConnections ( )
2024-08-22 13:31:39 -04:00
apiClient , err := NewAPIClientFromViper ( client )
2023-08-30 15:24:37 -04:00
if err != nil {
2023-10-24 17:00:49 -04:00
log . Fatal ( err )
2023-08-30 15:24:37 -04:00
}
2024-08-22 13:22:11 -04:00
webHandler := WebHandler {
2023-08-30 15:24:37 -04:00
ApiClient : apiClient ,
}
2023-03-21 18:48:23 -04:00
2024-08-22 13:22:11 -04:00
e . GET ( "/" , HandleIndex )
2023-03-21 18:37:51 -04:00
2024-08-22 13:22:11 -04:00
//e.GET("/a-propos", HandleAPropos)
2023-03-21 20:29:06 -04:00
2024-08-22 13:22:11 -04:00
//e.GET("/actualite", HandleActualite)
2023-03-21 20:29:06 -04:00
2024-08-22 13:22:11 -04:00
//e.GET("/actualite/:article", HandleActualiteArticle)
2023-03-21 20:29:06 -04:00
2024-08-22 13:22:11 -04:00
e . GET ( "/vie-etudiante" , HandleVieEtudiante )
2023-03-21 20:29:06 -04:00
2024-08-22 13:22:11 -04:00
e . GET ( "/vie-etudiante/:organisme" , HandleVieEtudianteOrganisme )
2023-03-21 20:29:06 -04:00
2023-08-30 15:24:37 -04:00
e . GET ( "/documentation" , webHandler . HandleDocumentation )
2023-03-21 20:29:06 -04:00
2024-08-22 13:22:11 -04:00
e . GET ( "/formulaires" , HandleFormulaires )
2023-03-21 20:29:06 -04:00
2023-04-26 18:27:58 -04:00
// Public Routes
2023-08-30 15:24:37 -04:00
e . GET ( "/public/documentation/:bucket/:document" , webHandler . HandlePublicDocumentation )
2023-04-26 18:27:58 -04:00
2023-04-26 19:15:22 -04:00
// Admin Routes
2024-08-22 13:22:11 -04:00
groupAdmin . GET ( "" , HandleAdmin )
2023-04-26 19:15:22 -04:00
2023-08-30 15:24:37 -04:00
groupAdmin . GET ( "/documents/upload" , webHandler . HandleAdminDocumentsUpload )
2023-04-26 19:28:20 -04:00
2023-08-30 15:24:37 -04:00
groupAdmin . POST ( "/documents/upload" , webHandler . HandleAdminDocumentsUploadPOST )
2023-04-26 19:43:43 -04:00
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 )
}
2024-08-22 13:22:11 -04:00
// versionCmd represents the version command
var versionCmd = & cobra . Command {
Use : "version" ,
Short : "Print version registered at build time" ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
fmt . Println ( "agecem-org" , version . Version ( ) )
} ,
}
func init ( ) {
rootCmd . AddCommand ( versionCmd )
}