From e94bd728ec7f709883fb232b2eb123543ba5660e Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 16 Aug 2021 15:30:14 +0200 Subject: Improve profile editing page & photo uploading --- S3.go | 216 ------------------------------------------------- config.json.example | 6 ++ directory.go | 13 ++- main.go | 28 ++++--- picture.go | 184 +++++++++++++++++++++++++++++++++++++++++ profile.go | 35 ++++---- static/image/34431.png | Bin 782061 -> 0 bytes templates/profile.html | 67 +++++++++------ 8 files changed, 279 insertions(+), 270 deletions(-) delete mode 100644 S3.go create mode 100644 picture.go delete mode 100644 static/image/34431.png diff --git a/S3.go b/S3.go deleted file mode 100644 index 8dd87b4..0000000 --- a/S3.go +++ /dev/null @@ -1,216 +0,0 @@ -package main - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "strconv" - - "image" - "image/jpeg" - _ "image/png" - - "mime/multipart" - "net/http" - "strings" - - "github.com/go-ldap/ldap/v3" - "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" -) - -const PROFILE_PICTURE_FIELD_NAME = "profilePicture" - -//Upload image through guichet server. -func uploadImage(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 - } - - buff := bytes.NewBuffer([]byte{}) - buff_thumbnail := bytes.NewBuffer([]byte{}) - err = resizeThumb(file, buff, buff_thumbnail) - if err != nil { - return "", err - } - - mc, err := newMinioClient() - if err != nil || mc == nil { - 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 - } - - _, err = mc.PutObject(context.Background(), config.S3_Bucket, name, buff_thumbnail, int64(buff_thumbnail.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{ - 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 - -} - -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] - switch fileType { - case "image": - return nil - default: - return errors.New("bad type") - } - -} - -func resizeThumb(file multipart.File, buff, buff_thumbnail *bytes.Buffer) error { - file.Seek(0, 0) - images, _, 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{ - Quality: 95, - }) - if err != nil { - return err - } - - err = jpeg.Encode(buff_thumbnail, images_thumbnail, &jpeg.Options{ - Quality: 95, - }) - - 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"] - //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{}) - 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/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/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 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 Binary files a/static/image/34431.png and /dev/null 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 @@ Retour
Photo de profil
- - Stack Overflow logo and icons and such - {{if .ErrorMessage}}
Impossible d'effectuer la modification.
{{ .ErrorMessage }}
@@ -20,43 +17,61 @@
{{end}}
-
- - -
-
- - +
+
+ + +
+
+ + +
-
- - -
-
- - -
-
- - -
+ +

Informations complémentaires

+ {{if .ProfilePicture}} +
+ + + +
+ {{end}} +
{{if .Visibility}} {{else}} {{end}} - +
-
-
+ +
+
+ - + +
+
+ +
+
+ +
+
+ + +
+
+ +
+ +
-- cgit v1.2.3