aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2021-08-16 15:30:14 +0200
committerAlex Auvolat <alex@adnab.me>2021-08-16 15:30:14 +0200
commite94bd728ec7f709883fb232b2eb123543ba5660e (patch)
treec7f75a9055e370df38dbba0c0f5b0c75a9dd649d
parent9e3279b9c04bb07d108fa820a72841b4082c0979 (diff)
downloadguichet-e94bd728ec7f709883fb232b2eb123543ba5660e.tar.gz
guichet-e94bd728ec7f709883fb232b2eb123543ba5660e.zip
Improve profile editing page & photo uploadingAdd_Directory
-rw-r--r--config.json.example6
-rw-r--r--directory.go13
-rw-r--r--main.go28
-rw-r--r--picture.go (renamed from S3.go)142
-rw-r--r--profile.go35
-rw-r--r--static/image/34431.pngbin782061 -> 0 bytes
-rw-r--r--templates/profile.html67
7 files changed, 150 insertions, 141 deletions
diff --git a/config.json.example b/config.json.example
index 2631acc..d24d131 100644
--- a/config.json.example
+++ b/config.json.example
@@ -24,5 +24,11 @@
"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
index c36bd41..b1e563d 100644
--- a/directory.go
+++ b/directory.go
@@ -10,6 +10,9 @@ import (
"github.com/gorilla/mux"
)
+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"))
@@ -51,8 +54,14 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
searchRequest := ldap.NewSearchRequest(
config.UserBaseDN,
ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false,
- "(&(objectclass=organizationalPerson)(visibility=on))",
- []string{config.UserNameAttr, "displayname", "mail", "description"},
+ "(&(objectclass=organizationalPerson)("+FIELD_NAME_DIRECTORY_VISIBILITY+"=on))",
+ []string{
+ config.UserNameAttr,
+ "displayname",
+ "mail",
+ "description",
+ FIELD_NAME_PROFILE_PICTURE,
+ },
nil)
sr, err := login.conn.Search(searchRequest)
diff --git a/main.go b/main.go
index 5754285..b5c8680 100644
--- a/main.go
+++ b/main.go
@@ -44,11 +44,11 @@ type ConfigFile struct {
GroupCanInvite string `json:"group_can_invite"`
GroupCanAdmin string `json:"group_can_admin"`
- S3_Endpoint string `json:"s3_endpoint"`
- S3_AccesKey string `json:"s3_acces_key"`
- S3_SecretKey string `json:"s3_secret_key"`
- S3_Region string `json:"s3_region"`
- S3_Bucket string `json:"s3_bucket"`
+ 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")
@@ -110,13 +110,13 @@ func main() {
r := mux.NewRouter()
r.HandleFunc("/", handleHome)
r.HandleFunc("/logout", handleLogout)
+
r.HandleFunc("/profile", handleProfile)
r.HandleFunc("/passwd", handlePasswd)
-
- r.HandleFunc("/image/{name}/{size}", handleDownloadImage)
+ r.HandleFunc("/picture/{name}", handleDownloadPicture)
r.HandleFunc("/directory", handleDirectory)
- r.HandleFunc("/search/{input}", handleSearch)
+ r.HandleFunc("/directory/search/{input}", handleSearch)
r.HandleFunc("/invite/new_account", handleInviteNewAccount)
r.HandleFunc("/invite/send_code", handleInviteSendCode)
@@ -226,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", "visibility", "description", PROFILE_PICTURE_FIELD_NAME},
+ []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/S3.go b/picture.go
index 8dd87b4..d3590c8 100644
--- a/S3.go
+++ b/picture.go
@@ -16,7 +16,6 @@ import (
"net/http"
"strings"
- "github.com/go-ldap/ldap/v3"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/minio/minio-go/v7"
@@ -24,10 +23,28 @@ import (
"github.com/nfnt/resize"
)
-const PROFILE_PICTURE_FIELD_NAME = "profilePicture"
+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 uploadImage(w http.ResponseWriter, r *http.Request, login *LoginStatus) (string, error) {
+func uploadProfilePicture(w http.ResponseWriter, r *http.Request, login *LoginStatus) (string, error) {
file, _, err := r.FormFile("image")
if err == http.ErrMissingFile {
@@ -37,14 +54,15 @@ func uploadImage(w http.ResponseWriter, r *http.Request, login *LoginStatus) (st
return "", err
}
defer file.Close()
+
err = checkImage(file)
if err != nil {
return "", err
}
- buff := bytes.NewBuffer([]byte{})
- buff_thumbnail := bytes.NewBuffer([]byte{})
- err = resizeThumb(file, buff, buff_thumbnail)
+ buffFull := bytes.NewBuffer([]byte{})
+ buffThumb := bytes.NewBuffer([]byte{})
+ err = resizePicture(file, buffFull, buffThumb)
if err != nil {
return "", err
}
@@ -54,52 +72,32 @@ func uploadImage(w http.ResponseWriter, r *http.Request, login *LoginStatus) (st
return "", err
}
- var name, nameFull string
-
- if nameConsul := login.UserEntry.GetAttributeValue(PROFILE_PICTURE_FIELD_NAME); nameConsul != "" {
- name = nameConsul
- nameFull = "full_" + name
- } else {
- name = uuid.New().String() + ".jpeg"
- nameFull = "full_" + name
+ // 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{})
}
- _, err = mc.PutObject(context.Background(), config.S3_Bucket, name, buff_thumbnail, int64(buff_thumbnail.Len()), minio.PutObjectOptions{
+ // 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.S3_Bucket, nameFull, buff, int64(buff.Len()), minio.PutObjectOptions{
+ _, err = mc.PutObject(context.Background(), config.S3Bucket, nameFull, buffFull, int64(buffFull.Len()), minio.PutObjectOptions{
ContentType: "image/jpeg",
})
if err != nil {
return "", err
}
- return name, nil
-}
-
-func newMinioClient() (*minio.Client, error) {
- endpoint := config.S3_Endpoint
- accessKeyID := config.S3_AccesKey
- secretKeyID := config.S3_SecretKey
- useSSL := true
-
- //Initialize Minio
- minioCLient, err := minio.New(endpoint, &minio.Options{
- Creds: credentials.NewStaticV4(accessKeyID, secretKeyID, ""),
- Secure: useSSL,
- Region: config.S3_Region,
- })
-
- if err != nil {
- return nil, err
- }
-
- return minioCLient, nil
-
+ return nameFull, nil
}
func checkImage(file multipart.File) error {
@@ -112,102 +110,72 @@ func checkImage(file multipart.File) error {
fileType := http.DetectContentType(buff)
fileType = strings.Split(fileType, "/")[0]
- switch fileType {
- case "image":
- return nil
- default:
+ if fileType != "image" {
return errors.New("bad type")
}
+ return nil
}
-func resizeThumb(file multipart.File, buff, buff_thumbnail *bytes.Buffer) error {
+func resizePicture(file multipart.File, buffFull, buffThumb *bytes.Buffer) error {
file.Seek(0, 0)
- images, _, err := image.Decode(file)
+ picture, _, err := image.Decode(file)
if err != nil {
return err
}
- buff.Reset()
- images = resize.Thumbnail(200, 200, images, resize.Lanczos3)
- images_thumbnail := resize.Thumbnail(80, 80, images, resize.Lanczos3)
- err = jpeg.Encode(buff, images, &jpeg.Options{
+ 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(buff_thumbnail, images_thumbnail, &jpeg.Options{
- Quality: 95,
+ err = jpeg.Encode(buffThumb, thumbnail, &jpeg.Options{
+ Quality: 100,
})
return err
}
-func handleDownloadImage(w http.ResponseWriter, r *http.Request) {
- //Get input value by user
- dn := mux.Vars(r)["name"]
- size := mux.Vars(r)["size"]
+func handleDownloadPicture(w http.ResponseWriter, r *http.Request) {
+ name := mux.Vars(r)["name"]
+
//Check login
login := checkLogin(w, r)
if login == nil {
return
}
- var imageName string
- if dn != "unknown_profile" {
- //Search values with ldap and filter
-
- searchRequest := ldap.NewSearchRequest(
- dn,
- ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
- "(objectclass=*)",
- []string{PROFILE_PICTURE_FIELD_NAME},
- nil)
-
- sr, err := login.conn.Search(searchRequest)
- if err != nil {
- http.Error(w, "Search: "+err.Error(), http.StatusInternalServerError)
- return
- }
- if len(sr.Entries) != 1 {
- http.Error(w, fmt.Sprintf("Not found user: %s cn: %s and numberEntries: %d", dn, strings.Split(dn, ",")[0], len(sr.Entries)), http.StatusInternalServerError)
- return
- }
- imageName = sr.Entries[0].GetAttributeValue(PROFILE_PICTURE_FIELD_NAME)
- if imageName == "" {
- http.Error(w, "User doesn't have profile image", http.StatusNotFound)
- return
- }
- } else {
- imageName = "unknown_profile.jpg"
- }
-
- if size == "full" {
- imageName = "full_" + imageName
- }
+
//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", imageName, minio.GetObjectOptions{})
+
+ 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 21cfe0a..4f5cf28 100644
--- a/profile.go
+++ b/profile.go
@@ -9,16 +9,16 @@ import (
)
type ProfileTplData struct {
- Status *LoginStatus
- ErrorMessage string
- Success bool
- Mail string
- DisplayName string
- GivenName string
- Surname string
- Visibility string
- Description string
- NameImage 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) {
@@ -39,8 +39,9 @@ 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("visibility")
+ 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" {
//5MB maximum size files
@@ -56,13 +57,13 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
}
data.Visibility = visible
- name, err := uploadImage(w, r, login)
+ profilePicture, err := uploadProfilePicture(w, r, login)
if err != nil {
data.ErrorMessage = err.Error()
}
- if name != "" {
- data.NameImage = name
+ if profilePicture != "" {
+ data.ProfilePicture = profilePicture
}
modify_request := ldap.NewModifyRequest(login.Info.DN, nil)
@@ -70,9 +71,9 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
modify_request.Replace("givenname", []string{data.GivenName})
modify_request.Replace("sn", []string{data.Surname})
modify_request.Replace("description", []string{data.Description})
- modify_request.Replace("visibility", []string{data.Visibility})
- if name != "" {
- modify_request.Replace(PROFILE_PICTURE_FIELD_NAME, []string{data.NameImage})
+ 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)
diff --git a/static/image/34431.png b/static/image/34431.png
deleted file mode 100644
index 2aba26b..0000000
--- a/static/image/34431.png
+++ /dev/null
Binary files differ
diff --git a/templates/profile.html b/templates/profile.html
index 92e229a..8704e23 100644
--- a/templates/profile.html
+++ b/templates/profile.html
@@ -6,9 +6,6 @@
<a class="ml-auto btn btn-info" href="/">Retour</a>
</div>
<h5>Photo de profil</h5>
-<object data="/image/{{ .Status.Info.DN}}/full" class=".img-thumbnail">
- <img src="/image/unknown_profile/full" alt="Stack Overflow logo and icons and such">
-</object>
{{if .ErrorMessage}}
<div class="alert alert-danger mt-4">Impossible d'effectuer la modification.
<div style="font-size: 0.8em">{{ .ErrorMessage }}</div>
@@ -20,43 +17,61 @@
</div>
{{end}}
<form method="POST" class="mt-4" enctype="multipart/form-data">
- <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 }}" />
+ <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 }}" />
- </div>
- <div class="form-group">
- <label for="surname">Nom de famille:</label>
- <input type="text" id="surname" name="surname" class="form-control" value="{{ .Surname }}" />
- </div>
- <div class="form-group">
- <label for="description">Description (180 caractères maximum)</label>
- <textarea id="description" name="description" class="form-control" maxlength="180">{{ .Description }}</textarea>
- </div>
+
+ <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ît sur l'annuaire</label>
+ <label class="form-check-label" for="visibility">Apparaître sur l'annuaire</label>
</div>
- <div class="form-group input-group mb-3">
- <div class="form-group custom-file">
+
+ <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">Choose picture (jpeg, jpg or png)</label>
+ <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="description">Description (180 caractères maximum)</label>
+ <textarea id="description" name="description" class="form-control" maxlength="180">{{ .Description }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Enregistrer les modifications</button>
</form>