Add_Directory_and_ProfilePicture #9
2
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)
|
||||
|
|
34
config.json.example
Normal file
|
@ -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"
|
||||
}
|
||||
|
121
directory.go
Normal file
|
@ -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)
|
||||
}
|
||||
|
||||
erwan marked this conversation as resolved
Outdated
|
||||
type SearchResult struct {
|
||||
erwan marked this conversation as resolved
Outdated
lx
commented
C'est le displayname que tu prends depuis le LDAP? Dans ce cas il faudrait appeller ça C'est le displayname que tu prends depuis le LDAP? Dans ce cas il faudrait appeller ça `DisplayName`
|
||||
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,
|
||||
erwan marked this conversation as resolved
Outdated
lx
commented
1. Ce n'est pas toujours `cn` l'attribut qui contient l'identifiant de la personne, c'est le paramètre `config.UserNameAttr` qui te donne le bon attribut à checker
2. On aimerait aussi chercher dans le displayname, et peut-être aussi le mail
|
||||
},
|
||||
nil)
|
||||
|
||||
erwan marked this conversation as resolved
Outdated
lx
commented
Idem, c'est pas Idem, c'est pas `cn` chez tout le monde
|
||||
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"),
|
||||
lx
commented
1. Pourquoi un résultat vide?
2. Si tu veux créer un résultat vide en Go, pas la peine de mettre tous les champs à `""`, si tu écris juste `SearchResult{}` ça crée une structure avec des chaines vides dans tous les champs
erwan
commented
Un résultat vide permet d'avoir un readyStateChange dans le JS ce qui permet d'initialiser le tableau à vide quand il n'y a plus de match. Pour le Un résultat vide permet d'avoir un readyStateChange dans le JS ce qui permet d'initialiser le tableau à vide quand il n'y a plus de match.
Pour le `go fmt`, j'utilise VsCode et à chaque sauvegarde il fait le `go fmt` et aussi de tous les warnings.
lx
commented
Mais si tu met pas le Mais si tu met pas le `SearchResult` vide, tu as quand même le readyStateChange non ? Et tu as un tableau JSON vide, c'est tout, mais ta partie JS est sensé gérer ça
erwan
commented
Justement au début j'avais fait ça. Ce qui me donnait une valeur
Elle n'était tout bonnement pas appelée. Justement au début j'avais fait ça. Ce qui me donnait une valeur `null`. Mais la fonction JS suivante ne répondez pas, elle ignorait juste la réponse. (même en enlevant le if)
```js
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 201) {
```
Elle n'était tout bonnement pas appelée.
|
||||
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]
|
||||
}
|
6
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
|
||||
)
|
||||
|
|
43
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=
|
||||
|
|
53
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"`
|
||||
erwan marked this conversation as resolved
Outdated
lx
commented
Endpoint de quoi? Il faudrait appeller ça Endpoint de quoi? Il faudrait appeller ça `S3Endpoint` pour être clair, et les autres les appeller `S3AccessKey` et `S3SecretKey`.
|
||||
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)
|
||||
|
|
184
picture.go
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
46
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)
|
||||
|
|
5
static/javascript/minio.js
Normal file
|
@ -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
|
||||
})
|
24
static/javascript/search.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
23
templates/directory.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{{define "title"}}Annuaire |{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="d-flex">
|
||||
<h4>Annuaire</h4>
|
||||
<a class="ml-auto btn btn-info" href="/">Menu principal</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<form>
|
||||
<div class="form-group form-row">
|
||||
<div class="col-sm-2"> </div>
|
||||
<label for="search" class="col-sm-2 col-form-label">Rechercher :</label>
|
||||
<input class="form-control col-sm-4" id="search" name="search" type="text" onkeyup="searchDirectory()" size="20">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="search-results"></div>
|
||||
|
||||
<script src="/static/javascript/search.js"></script>
|
||||
|
||||
{{end}}
|
23
templates/directory_results.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{{if .Results}}
|
||||
{{range .Results}}
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<div class="float-right">
|
||||
{{if .ProfilePicture}}
|
||||
<a href="/picture/{{.ProfilePicture}}">
|
||||
<img src="/picture/{{.ProfilePicture}}-thumb"/>
|
||||
</a>
|
||||
{{else}}
|
||||
{{end}}
|
||||
</div>
|
||||
<h5 class="card-title">
|
||||
{{.DisplayName}}
|
||||
<code>{{.Id}}@</code>
|
||||
</h5>
|
||||
<p class="card-text">{{.Description}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
Aucun résultat.
|
||||
{{end}}
|
|
@ -16,6 +16,7 @@
|
|||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="/profile">Modifier mon profil</a>
|
||||
<a class="list-group-item list-group-item-action" href="/passwd">Modifier mon mot de passe</a>
|
||||
<a class="list-group-item list-group-item-action" href="/directory">Annuaire</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -16,27 +16,64 @@
|
|||
Profil enregistré.
|
||||
</div>
|
||||
{{end}}
|
||||
<form method="POST" class="mt-4">
|
||||
<div class="form-group">
|
||||
<label>Nom d'utilisateur:</label>
|
||||
<input type="text" disabled="true" class="form-control" value="{{ .Status.Info.Username }}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="mail">Adresse e-mail:</label>
|
||||
<input type="text" id="mail" disabled="true" name="mail" class="form-control" value="{{ .Mail }}" />
|
||||
<form method="POST" class="mt-4" enctype="multipart/form-data">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label>Nom d'utilisateur:</label>
|
||||
<input type="text" disabled="true" class="form-control" value="{{ .Status.Info.Username }}" />
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="mail">Adresse e-mail:</label>
|
||||
<input type="text" id="mail" disabled="true" name="mail" class="form-control" value="{{ .Mail }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="display_name">Nom complet:</label>
|
||||
<input type="text" id="display_name" name="display_name" class="form-control" value="{{ .DisplayName }}" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="given_name">Prénom:</label>
|
||||
<input type="text" id="given_name" name="given_name" class="form-control" value="{{ .GivenName }}" />
|
||||
|
||||
<h4>Informations complémentaires</h4>
|
||||
{{if .ProfilePicture}}
|
||||
<div class="float-right">
|
||||
<a href="/picture/{{.ProfilePicture}}">
|
||||
<img src="/picture/{{.ProfilePicture}}-thumb" />
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="form-group form-check">
|
||||
{{if .Visibility}}
|
||||
<input class="form-check-input" name="visibility" type="checkbox" id="visibility" value="on" checked>
|
||||
{{else}}
|
||||
<input class="form-check-input" name="visibility" type="checkbox" id="visibility">
|
||||
{{end}}
|
||||
<label class="form-check-label" for="visibility">Apparaître sur l'annuaire</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-8 input-group mb-3 custom-file">
|
||||
<label for="image">Photo de profil:</label>
|
||||
<input type="file" name="image" class="custom-file-input" id="image">
|
||||
<label class="custom-file-label" for="image">Photo de profil (jpeg, jpg or png)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="given_name">Prénom:</label>
|
||||
<input type="text" id="given_name" name="given_name" class="form-control" value="{{ .GivenName }}" />
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="surname">Nom de famille:</label>
|
||||
<input type="text" id="surname" name="surname" class="form-control" value="{{ .Surname }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="surname">Nom de famille:</label>
|
||||
<input type="text" id="surname" name="surname" class="form-control" value="{{ .Surname }}" />
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" class="form-control">{{ .Description }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Enregistrer les modifications</button>
|
||||
</form>
|
||||
<script src="/static/javascript/minio.js"></script>
|
||||
{{end}}
|
||||
|
|
L'identifiant, c'est le CN ? Dans tous les cas
Identifiant
c'est un mot français, il faudrait appeller ça plutôt justeId
.