diff --git a/Makefile b/Makefile index 62311e9..e42acb7 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ BIN=guichet -SRC=main.go ssha.go profile.go admin.go invite.go +SRC=main.go ssha.go profile.go admin.go invite.go directory.go picture.go DOCKER=lxpz/guichet_amd64 all: $(BIN) diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..d24d131 --- /dev/null +++ b/config.json.example @@ -0,0 +1,34 @@ +{ + "http_bind_addr": ":9991", + "ldap_server_addr": "ldap://127.0.0.1:389", + + "base_dn": "dc=example,dc=com", + "user_base_dn": "ou=users,dc=example,dc=com", + "user_name_attr": "uid", + "group_base_dn": "ou=groups,dc=example,dc=com", + "group_name_attr": "gid", + + "invitation_base_dn": "ou=invitations,dc=example,dc=com", + "invitation_name_attr": "cn", + "invited_mail_format": "{}@example.com", + "invited_auto_groups": [ + "cn=email,ou=groups,dc=example,dc=com" + ], + + "web_address": "https://guichet.example.com", + "mail_from": "welcome@example.com", + "smtp_server": "smtp.example.com", + "smtp_username": "guichet", + "smtp_password": "", + + "admin_account": "uid=admin,dc=example,dc=com", + "group_can_admin": "gid=admin,ou=groups,dc=example,dc=com", + "group_can_invite": "" + + "s3_endpoint": "garage.example.com", + "s3_access_key": "", + "s3_secret_key": "", + "s3_region": "garage", + "s3_bucket": "bottin-pictures" +} + diff --git a/directory.go b/directory.go new file mode 100644 index 0000000..06afdf2 --- /dev/null +++ b/directory.go @@ -0,0 +1,121 @@ +package main + +import ( + "html/template" + "net/http" + "sort" + "strings" + + "github.com/go-ldap/ldap/v3" +) + +const FIELD_NAME_PROFILE_PICTURE = "profilePicture" +const FIELD_NAME_DIRECTORY_VISIBILITY = "directoryVisibility" + +func handleDirectory(w http.ResponseWriter, r *http.Request) { + templateDirectory := template.Must(template.ParseFiles("templates/layout.html", "templates/directory.html")) + + login := checkLogin(w, r) + if login == nil { + return + } + + templateDirectory.Execute(w, nil) +} + +type SearchResult struct { + DN string + Id string + DisplayName string + Email string + Description string + ProfilePicture string +} + +type SearchResults struct { + Results []SearchResult +} + +func handleDirectorySearch(w http.ResponseWriter, r *http.Request) { + templateDirectoryResults := template.Must(template.ParseFiles("templates/directory_results.html")) + + //Get input value by user + r.ParseMultipartForm(1024) + input := strings.TrimSpace(strings.Join(r.Form["query"], "")) + + if r.Method != "POST" || input == "" { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + //Log to allow the research + login := checkLogin(w, r) + if login == nil { + http.Error(w, "Login required", http.StatusUnauthorized) + return + } + + //Search values with ldap and filter + searchRequest := ldap.NewSearchRequest( + config.UserBaseDN, + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, + "(&(objectclass=organizationalPerson)("+FIELD_NAME_DIRECTORY_VISIBILITY+"=on))", + []string{ + config.UserNameAttr, + "displayname", + "mail", + "description", + FIELD_NAME_PROFILE_PICTURE, + }, + nil) + + sr, err := login.conn.Search(searchRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + //Transform the researh's result in a correct struct to send JSON + results := []SearchResult{} + + for _, values := range sr.Entries { + if ContainsI(values.GetAttributeValue(config.UserNameAttr), input) || + ContainsI(values.GetAttributeValue("displayname"), input) || + ContainsI(values.GetAttributeValue("mail"), input) { + results = append(results, SearchResult{ + DN: values.DN, + Id: values.GetAttributeValue(config.UserNameAttr), + DisplayName: values.GetAttributeValue("displayname"), + Email: values.GetAttributeValue("mail"), + Description: values.GetAttributeValue("description"), + ProfilePicture: values.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE), + }) + } + } + + search_results := SearchResults{ + Results: results, + } + sort.Sort(&search_results) + + templateDirectoryResults.Execute(w, search_results) +} + +func ContainsI(a string, b string) bool { + return strings.Contains( + strings.ToLower(a), + strings.ToLower(b), + ) +} + +func (r *SearchResults) Len() int { + return len(r.Results) +} + +func (r *SearchResults) Less(i, j int) bool { + return r.Results[i].Id < r.Results[j].Id +} + +func (r *SearchResults) Swap(i, j int) { + r.Results[i], r.Results[j] = r.Results[j], r.Results[i] +} diff --git a/go.mod b/go.mod index 4930fb2..781b3ce 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,12 @@ require ( github.com/emersion/go-smtp v0.12.1 github.com/go-ldap/ldap v3.0.3+incompatible github.com/go-ldap/ldap/v3 v3.1.6 + github.com/google/uuid v1.1.1 github.com/gorilla/mux v1.7.3 github.com/gorilla/sessions v1.2.0 - github.com/sirupsen/logrus v1.4.2 + github.com/minio/minio-go/v7 v7.0.0 + github.com/sirupsen/logrus v1.6.0 + github.com/stretchr/objx v0.1.1 // indirect golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 ) diff --git a/go.sum b/go.sum index 49da79d..9594bab 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs= github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= @@ -10,25 +12,66 @@ github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHj github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-ldap/ldap/v3 v3.1.6 h1:VTihvB7egSAvU6KOagaiA/EvgJMR2jsjRAVIho2ydBo= github.com/go-ldap/ldap/v3 v3.1.6/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs= +github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= +github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= +github.com/minio/minio-go/v7 v7.0.0 h1:99hRCmsmMi+hKK93C26iPnRQebTsdK8GEx8Xb4XLr7I= +github.com/minio/minio-go/v7 v7.0.0/go.mod h1:dJ80Mv2HeGkYLH1sqS/ksz07ON6csH3S6JUMSQ2zAns= +github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= +github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg= golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/main.go b/main.go index 7126826..d574f3f 100644 --- a/main.go +++ b/main.go @@ -43,6 +43,12 @@ type ConfigFile struct { AdminAccount string `json:"admin_account"` GroupCanInvite string `json:"group_can_invite"` GroupCanAdmin string `json:"group_can_admin"` + + S3Endpoint string `json:"s3_endpoint"` + S3AccessKey string `json:"s3_access_key"` + S3SecretKey string `json:"s3_secret_key"` + S3Region string `json:"s3_region"` + S3Bucket string `json:"s3_bucket"` } var configFlag = flag.String("config", "./config.json", "Configuration file path") @@ -54,47 +60,21 @@ const SESSION_NAME = "guichet_session" var store sessions.Store = nil func readConfig() ConfigFile { + // Default configuration values for certain fields config_file := ConfigFile{ HttpBindAddr: ":9991", LdapServerAddr: "ldap://127.0.0.1:389", - LdapTLS: false, - BaseDN: "dc=example,dc=com", - UserBaseDN: "ou=users,dc=example,dc=com", UserNameAttr: "uid", - GroupBaseDN: "ou=groups,dc=example,dc=com", GroupNameAttr: "gid", - InvitationBaseDN: "ou=invitations,dc=example,dc=com", InvitationNameAttr: "cn", - InvitedMailFormat: "{}@example.com", InvitedAutoGroups: []string{}, - - WebAddress: "https://guichet.example.com", - MailFrom: "guichet@example.com", - SMTPServer: "smtp.example.com", - - AdminAccount: "uid=admin,dc=example,dc=com", - GroupCanInvite: "", - GroupCanAdmin: "gid=admin,ou=groups,dc=example,dc=com", } _, err := os.Stat(*configFlag) if os.IsNotExist(err) { - // Generate default config file - log.Printf("Generating default config file as %s", *configFlag) - - bytes, err := json.MarshalIndent(&config_file, "", " ") - if err != nil { - log.Fatal(err) - } - - err = ioutil.WriteFile(*configFlag, bytes, 0644) - if err != nil { - log.Fatal(err) - } - - return config_file + log.Fatalf("Could not find Guichet configuration file at %s. Please create this file, for example starting with config.json.example and customizing it for your deployment.", *configFlag) } if err != nil { @@ -130,8 +110,13 @@ func main() { r := mux.NewRouter() r.HandleFunc("/", handleHome) r.HandleFunc("/logout", handleLogout) + r.HandleFunc("/profile", handleProfile) r.HandleFunc("/passwd", handlePasswd) + r.HandleFunc("/picture/{name}", handleDownloadPicture) + + r.HandleFunc("/directory/search", handleDirectorySearch) + r.HandleFunc("/directory", handleDirectory) r.HandleFunc("/invite/new_account", handleInviteNewAccount) r.HandleFunc("/invite/send_code", handleInviteSendCode) @@ -241,7 +226,17 @@ func checkLogin(w http.ResponseWriter, r *http.Request) *LoginStatus { login_info.DN, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, requestKind, - []string{"dn", "displayname", "givenname", "sn", "mail", "memberof"}, + []string{ + "dn", + "displayname", + "givenname", + "sn", + "mail", + "memberof", + "description", + FIELD_NAME_DIRECTORY_VISIBILITY, + FIELD_NAME_PROFILE_PICTURE, + }, nil) sr, err := l.Search(searchRequest) diff --git a/picture.go b/picture.go new file mode 100644 index 0000000..d3590c8 --- /dev/null +++ b/picture.go @@ -0,0 +1,184 @@ +package main + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "strconv" + + "image" + "image/jpeg" + _ "image/png" + + "mime/multipart" + "net/http" + "strings" + + "github.com/google/uuid" + "github.com/gorilla/mux" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/nfnt/resize" +) + +func newMinioClient() (*minio.Client, error) { + endpoint := config.S3Endpoint + accessKeyID := config.S3AccessKey + secretKeyID := config.S3SecretKey + useSSL := true + + //Initialize Minio + minioCLient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKeyID, secretKeyID, ""), + Secure: useSSL, + Region: config.S3Region, + }) + + if err != nil { + return nil, err + } + + return minioCLient, nil +} + +//Upload image through guichet server. +func uploadProfilePicture(w http.ResponseWriter, r *http.Request, login *LoginStatus) (string, error) { + file, _, err := r.FormFile("image") + + if err == http.ErrMissingFile { + return "", nil + } + if err != nil { + return "", err + } + defer file.Close() + + err = checkImage(file) + if err != nil { + return "", err + } + + buffFull := bytes.NewBuffer([]byte{}) + buffThumb := bytes.NewBuffer([]byte{}) + err = resizePicture(file, buffFull, buffThumb) + if err != nil { + return "", err + } + + mc, err := newMinioClient() + if err != nil || mc == nil { + return "", err + } + + // If a previous profile picture existed, delete it + // (don't care about errors) + if nameConsul := login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE); nameConsul != "" { + mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul, minio.RemoveObjectOptions{}) + mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul+"-thumb", minio.RemoveObjectOptions{}) + } + + // Generate new random name for picture + nameFull := uuid.New().String() + nameThumb := nameFull + "-thumb" + + _, err = mc.PutObject(context.Background(), config.S3Bucket, nameThumb, buffThumb, int64(buffThumb.Len()), minio.PutObjectOptions{ + ContentType: "image/jpeg", + }) + if err != nil { + return "", err + } + + _, err = mc.PutObject(context.Background(), config.S3Bucket, nameFull, buffFull, int64(buffFull.Len()), minio.PutObjectOptions{ + ContentType: "image/jpeg", + }) + if err != nil { + return "", err + } + + return nameFull, nil +} + +func checkImage(file multipart.File) error { + buff := make([]byte, 512) //Detect read only the first 512 bytes + _, err := file.Read(buff) + if err != nil { + return err + } + file.Seek(0, 0) + + fileType := http.DetectContentType(buff) + fileType = strings.Split(fileType, "/")[0] + if fileType != "image" { + return errors.New("bad type") + } + + return nil +} + +func resizePicture(file multipart.File, buffFull, buffThumb *bytes.Buffer) error { + file.Seek(0, 0) + picture, _, err := image.Decode(file) + if err != nil { + return err + } + + thumbnail := resize.Thumbnail(90, 90, picture, resize.Lanczos3) + picture = resize.Thumbnail(480, 480, picture, resize.Lanczos3) + + err = jpeg.Encode(buffFull, picture, &jpeg.Options{ + Quality: 95, + }) + if err != nil { + return err + } + + err = jpeg.Encode(buffThumb, thumbnail, &jpeg.Options{ + Quality: 100, + }) + + return err +} + +func handleDownloadPicture(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + + //Check login + login := checkLogin(w, r) + if login == nil { + return + } + + //Get the object after connect MC + mc, err := newMinioClient() + if err != nil { + http.Error(w, "MinioClient: "+err.Error(), http.StatusInternalServerError) + return + } + + obj, err := mc.GetObject(context.Background(), "bottin-pictures", name, minio.GetObjectOptions{}) + if err != nil { + http.Error(w, "MinioClient: GetObject: "+err.Error(), http.StatusInternalServerError) + return + } + defer obj.Close() + + objStat, err := obj.Stat() + if err != nil { + http.Error(w, "MiniObjet: "+err.Error(), http.StatusInternalServerError) + return + } + + //Send JSON through xhttp + w.Header().Set("Content-Type", objStat.ContentType) + w.Header().Set("Content-Length", strconv.Itoa(int(objStat.Size))) + //Copy obj in w + writting, err := io.Copy(w, obj) + + if writting != objStat.Size || err != nil { + http.Error(w, fmt.Sprintf("WriteBody: %s, bytes wrote %d on %d", err.Error(), writting, objStat.Size), http.StatusInternalServerError) + return + } + +} diff --git a/profile.go b/profile.go index e7a54bd..4f5cf28 100644 --- a/profile.go +++ b/profile.go @@ -9,13 +9,16 @@ import ( ) type ProfileTplData struct { - Status *LoginStatus - ErrorMessage string - Success bool - Mail string - DisplayName string - GivenName string - Surname string + Status *LoginStatus + ErrorMessage string + Success bool + Mail string + DisplayName string + GivenName string + Surname string + Visibility string + Description string + ProfilePicture string } func handleProfile(w http.ResponseWriter, r *http.Request) { @@ -36,25 +39,50 @@ func handleProfile(w http.ResponseWriter, r *http.Request) { data.DisplayName = login.UserEntry.GetAttributeValue("displayname") data.GivenName = login.UserEntry.GetAttributeValue("givenname") data.Surname = login.UserEntry.GetAttributeValue("sn") + data.Visibility = login.UserEntry.GetAttributeValue(FIELD_NAME_DIRECTORY_VISIBILITY) + data.Description = login.UserEntry.GetAttributeValue("description") + data.ProfilePicture = login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE) if r.Method == "POST" { - r.ParseForm() + //5MB maximum size files + r.ParseMultipartForm(5 << 20) data.DisplayName = strings.TrimSpace(strings.Join(r.Form["display_name"], "")) data.GivenName = strings.TrimSpace(strings.Join(r.Form["given_name"], "")) data.Surname = strings.TrimSpace(strings.Join(r.Form["surname"], "")) + data.Description = strings.Trim(strings.Join(r.Form["description"], ""), "") + visible := strings.TrimSpace(strings.Join(r.Form["visibility"], "")) + if visible != "" { + visible = "on" + } + data.Visibility = visible + + profilePicture, err := uploadProfilePicture(w, r, login) + if err != nil { + data.ErrorMessage = err.Error() + } + + if profilePicture != "" { + data.ProfilePicture = profilePicture + } modify_request := ldap.NewModifyRequest(login.Info.DN, nil) modify_request.Replace("displayname", []string{data.DisplayName}) modify_request.Replace("givenname", []string{data.GivenName}) modify_request.Replace("sn", []string{data.Surname}) + modify_request.Replace("description", []string{data.Description}) + modify_request.Replace(FIELD_NAME_DIRECTORY_VISIBILITY, []string{data.Visibility}) + if data.ProfilePicture != "" { + modify_request.Replace(FIELD_NAME_PROFILE_PICTURE, []string{data.ProfilePicture}) + } - err := login.conn.Modify(modify_request) + err = login.conn.Modify(modify_request) if err != nil { data.ErrorMessage = err.Error() } else { data.Success = true } + } templateProfile.Execute(w, data) diff --git a/static/javascript/minio.js b/static/javascript/minio.js new file mode 100644 index 0000000..cea2a18 --- /dev/null +++ b/static/javascript/minio.js @@ -0,0 +1,5 @@ +document.querySelector('.custom-file-input').addEventListener('change',function(e){ + var fileName = document.getElementById("image").files[0].name; + var nextSibling = e.target.nextElementSibling + nextSibling.innerText = fileName + }) diff --git a/static/javascript/search.js b/static/javascript/search.js new file mode 100644 index 0000000..2a75889 --- /dev/null +++ b/static/javascript/search.js @@ -0,0 +1,24 @@ +var last_id = 0; + +function searchDirectory() { + var input = document.getElementById("search").value; + if(input){ + last_id++; + var request_id = last_id; + + var data = new FormData(); + data.append("query", input); + + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (request_id != last_id) return; + + if (this.readyState == 4 && this.status == 200) { + var result_div = document.getElementById("search-results"); + result_div.innerHTML = xhttp.responseText; + } + }; + xhttp.open("POST", "/directory/search", true); + xhttp.send(data); + } +} diff --git a/templates/directory.html b/templates/directory.html new file mode 100644 index 0000000..d995fb2 --- /dev/null +++ b/templates/directory.html @@ -0,0 +1,23 @@ +{{define "title"}}Annuaire |{{end}} + +{{define "body"}} +
+

Annuaire

+ Menu principal +
+ + + +
+
+
 
+ + +
+
+ +
+ + + +{{end}} diff --git a/templates/directory_results.html b/templates/directory_results.html new file mode 100644 index 0000000..c7dd715 --- /dev/null +++ b/templates/directory_results.html @@ -0,0 +1,23 @@ +{{if .Results}} + {{range .Results}} +
+
+
+ {{if .ProfilePicture}} + + + + {{else}} + {{end}} +
+
+ {{.DisplayName}} + {{.Id}}@ +
+

{{.Description}}

+
+
+ {{end}} +{{else}} + Aucun résultat. +{{end}} diff --git a/templates/home.html b/templates/home.html index 5556ba7..7d8cc99 100644 --- a/templates/home.html +++ b/templates/home.html @@ -16,6 +16,7 @@
Modifier mon profil Modifier mon mot de passe + Annuaire
diff --git a/templates/profile.html b/templates/profile.html index bfd79cd..edf9d76 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -16,27 +16,64 @@ Profil enregistré. {{end}} -
-
- - -
-
- - + +
+
+ + +
+
+ + +
-
- - + +

Informations complémentaires

+ {{if .ProfilePicture}} +
+ + + +
+ {{end}} + +
+ {{if .Visibility}} + + {{else}} + + {{end}} +
+ +
+
+ + + +
+
+ +
+
+ + +
+
+ + +
+
+
- - + +
+ {{end}}