Improve profile editing page & photo uploading

This commit is contained in:
Alex 2021-08-16 15:30:14 +02:00
parent 9e3279b9c0
commit e94bd728ec
No known key found for this signature in database
GPG key ID: EDABF9711E244EB1
7 changed files with 149 additions and 140 deletions

View file

@ -24,5 +24,11 @@
"admin_account": "uid=admin,dc=example,dc=com", "admin_account": "uid=admin,dc=example,dc=com",
"group_can_admin": "gid=admin,ou=groups,dc=example,dc=com", "group_can_admin": "gid=admin,ou=groups,dc=example,dc=com",
"group_can_invite": "" "group_can_invite": ""
"s3_endpoint": "garage.example.com",
"s3_access_key": "",
"s3_secret_key": "",
"s3_region": "garage",
"s3_bucket": "bottin-pictures"
} }

View file

@ -10,6 +10,9 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
const FIELD_NAME_PROFILE_PICTURE = "profilePicture"
const FIELD_NAME_DIRECTORY_VISIBILITY = "directoryVisibility"
func handleDirectory(w http.ResponseWriter, r *http.Request) { func handleDirectory(w http.ResponseWriter, r *http.Request) {
templateDirectory := template.Must(template.ParseFiles("templates/layout.html", "templates/directory.html")) 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( searchRequest := ldap.NewSearchRequest(
config.UserBaseDN, config.UserBaseDN,
ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false,
"(&(objectclass=organizationalPerson)(visibility=on))", "(&(objectclass=organizationalPerson)("+FIELD_NAME_DIRECTORY_VISIBILITY+"=on))",
[]string{config.UserNameAttr, "displayname", "mail", "description"}, []string{
config.UserNameAttr,
"displayname",
"mail",
"description",
FIELD_NAME_PROFILE_PICTURE,
},
nil) nil)
sr, err := login.conn.Search(searchRequest) sr, err := login.conn.Search(searchRequest)

28
main.go
View file

@ -44,11 +44,11 @@ type ConfigFile struct {
GroupCanInvite string `json:"group_can_invite"` GroupCanInvite string `json:"group_can_invite"`
GroupCanAdmin string `json:"group_can_admin"` GroupCanAdmin string `json:"group_can_admin"`
S3_Endpoint string `json:"s3_endpoint"` S3Endpoint string `json:"s3_endpoint"`
S3_AccesKey string `json:"s3_acces_key"` S3AccessKey string `json:"s3_access_key"`
S3_SecretKey string `json:"s3_secret_key"` S3SecretKey string `json:"s3_secret_key"`
S3_Region string `json:"s3_region"` S3Region string `json:"s3_region"`
S3_Bucket string `json:"s3_bucket"` S3Bucket string `json:"s3_bucket"`
} }
var configFlag = flag.String("config", "./config.json", "Configuration file path") var configFlag = flag.String("config", "./config.json", "Configuration file path")
@ -110,13 +110,13 @@ func main() {
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/", handleHome) r.HandleFunc("/", handleHome)
r.HandleFunc("/logout", handleLogout) r.HandleFunc("/logout", handleLogout)
r.HandleFunc("/profile", handleProfile) r.HandleFunc("/profile", handleProfile)
r.HandleFunc("/passwd", handlePasswd) r.HandleFunc("/passwd", handlePasswd)
r.HandleFunc("/picture/{name}", handleDownloadPicture)
r.HandleFunc("/image/{name}/{size}", handleDownloadImage)
r.HandleFunc("/directory", handleDirectory) r.HandleFunc("/directory", handleDirectory)
r.HandleFunc("/search/{input}", handleSearch) r.HandleFunc("/directory/search/{input}", handleSearch)
r.HandleFunc("/invite/new_account", handleInviteNewAccount) r.HandleFunc("/invite/new_account", handleInviteNewAccount)
r.HandleFunc("/invite/send_code", handleInviteSendCode) r.HandleFunc("/invite/send_code", handleInviteSendCode)
@ -226,7 +226,17 @@ func checkLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
login_info.DN, login_info.DN,
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
requestKind, 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) nil)
sr, err := l.Search(searchRequest) sr, err := l.Search(searchRequest)

View file

@ -16,7 +16,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/go-ldap/ldap/v3"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
@ -24,10 +23,28 @@ import (
"github.com/nfnt/resize" "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. //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") file, _, err := r.FormFile("image")
if err == http.ErrMissingFile { if err == http.ErrMissingFile {
@ -37,14 +54,15 @@ func uploadImage(w http.ResponseWriter, r *http.Request, login *LoginStatus) (st
return "", err return "", err
} }
defer file.Close() defer file.Close()
err = checkImage(file) err = checkImage(file)
if err != nil { if err != nil {
return "", err return "", err
} }
buff := bytes.NewBuffer([]byte{}) buffFull := bytes.NewBuffer([]byte{})
buff_thumbnail := bytes.NewBuffer([]byte{}) buffThumb := bytes.NewBuffer([]byte{})
err = resizeThumb(file, buff, buff_thumbnail) err = resizePicture(file, buffFull, buffThumb)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -54,52 +72,32 @@ func uploadImage(w http.ResponseWriter, r *http.Request, login *LoginStatus) (st
return "", err return "", err
} }
var name, nameFull string // If a previous profile picture existed, delete it
// (don't care about errors)
if nameConsul := login.UserEntry.GetAttributeValue(PROFILE_PICTURE_FIELD_NAME); nameConsul != "" { if nameConsul := login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE); nameConsul != "" {
name = nameConsul mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul, minio.RemoveObjectOptions{})
nameFull = "full_" + name mc.RemoveObject(context.Background(), config.S3Bucket, nameConsul+"-thumb", minio.RemoveObjectOptions{})
} 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{ // 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", ContentType: "image/jpeg",
}) })
if err != nil { if err != nil {
return "", err 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", ContentType: "image/jpeg",
}) })
if err != nil { if err != nil {
return "", err return "", err
} }
return name, nil return nameFull, 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 { func checkImage(file multipart.File) error {
@ -112,102 +110,72 @@ func checkImage(file multipart.File) error {
fileType := http.DetectContentType(buff) fileType := http.DetectContentType(buff)
fileType = strings.Split(fileType, "/")[0] fileType = strings.Split(fileType, "/")[0]
switch fileType { if fileType != "image" {
case "image":
return nil
default:
return errors.New("bad type") 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) file.Seek(0, 0)
images, _, err := image.Decode(file) picture, _, err := image.Decode(file)
if err != nil { if err != nil {
return err 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, Quality: 95,
}) })
if err != nil { if err != nil {
return err return err
} }
err = jpeg.Encode(buff_thumbnail, images_thumbnail, &jpeg.Options{ err = jpeg.Encode(buffThumb, thumbnail, &jpeg.Options{
Quality: 95, Quality: 100,
}) })
return err return err
} }
func handleDownloadImage(w http.ResponseWriter, r *http.Request) { func handleDownloadPicture(w http.ResponseWriter, r *http.Request) {
//Get input value by user name := mux.Vars(r)["name"]
dn := mux.Vars(r)["name"]
size := mux.Vars(r)["size"]
//Check login //Check login
login := checkLogin(w, r) login := checkLogin(w, r)
if login == nil { if login == nil {
return 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 //Get the object after connect MC
mc, err := newMinioClient() mc, err := newMinioClient()
if err != nil { if err != nil {
http.Error(w, "MinioClient: "+err.Error(), http.StatusInternalServerError) http.Error(w, "MinioClient: "+err.Error(), http.StatusInternalServerError)
return 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 { if err != nil {
http.Error(w, "MinioClient: GetObject: "+err.Error(), http.StatusInternalServerError) http.Error(w, "MinioClient: GetObject: "+err.Error(), http.StatusInternalServerError)
return return
} }
defer obj.Close() defer obj.Close()
objStat, err := obj.Stat() objStat, err := obj.Stat()
if err != nil { if err != nil {
http.Error(w, "MiniObjet: "+err.Error(), http.StatusInternalServerError) http.Error(w, "MiniObjet: "+err.Error(), http.StatusInternalServerError)
return return
} }
//Send JSON through xhttp //Send JSON through xhttp
w.Header().Set("Content-Type", objStat.ContentType) w.Header().Set("Content-Type", objStat.ContentType)
w.Header().Set("Content-Length", strconv.Itoa(int(objStat.Size))) w.Header().Set("Content-Length", strconv.Itoa(int(objStat.Size)))
//Copy obj in w //Copy obj in w
writting, err := io.Copy(w, obj) writting, err := io.Copy(w, obj)
if writting != objStat.Size || err != nil { 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) http.Error(w, fmt.Sprintf("WriteBody: %s, bytes wrote %d on %d", err.Error(), writting, objStat.Size), http.StatusInternalServerError)
return return

View file

@ -9,16 +9,16 @@ import (
) )
type ProfileTplData struct { type ProfileTplData struct {
Status *LoginStatus Status *LoginStatus
ErrorMessage string ErrorMessage string
Success bool Success bool
Mail string Mail string
DisplayName string DisplayName string
GivenName string GivenName string
Surname string Surname string
Visibility string Visibility string
Description string Description string
NameImage string ProfilePicture string
} }
func handleProfile(w http.ResponseWriter, r *http.Request) { 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.DisplayName = login.UserEntry.GetAttributeValue("displayname")
data.GivenName = login.UserEntry.GetAttributeValue("givenname") data.GivenName = login.UserEntry.GetAttributeValue("givenname")
data.Surname = login.UserEntry.GetAttributeValue("sn") 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.Description = login.UserEntry.GetAttributeValue("description")
data.ProfilePicture = login.UserEntry.GetAttributeValue(FIELD_NAME_PROFILE_PICTURE)
if r.Method == "POST" { if r.Method == "POST" {
//5MB maximum size files //5MB maximum size files
@ -56,13 +57,13 @@ func handleProfile(w http.ResponseWriter, r *http.Request) {
} }
data.Visibility = visible data.Visibility = visible
name, err := uploadImage(w, r, login) profilePicture, err := uploadProfilePicture(w, r, login)
if err != nil { if err != nil {
data.ErrorMessage = err.Error() data.ErrorMessage = err.Error()
} }
if name != "" { if profilePicture != "" {
data.NameImage = name data.ProfilePicture = profilePicture
} }
modify_request := ldap.NewModifyRequest(login.Info.DN, nil) 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("givenname", []string{data.GivenName})
modify_request.Replace("sn", []string{data.Surname}) modify_request.Replace("sn", []string{data.Surname})
modify_request.Replace("description", []string{data.Description}) modify_request.Replace("description", []string{data.Description})
modify_request.Replace("visibility", []string{data.Visibility}) modify_request.Replace(FIELD_NAME_DIRECTORY_VISIBILITY, []string{data.Visibility})
if name != "" { if data.ProfilePicture != "" {
modify_request.Replace(PROFILE_PICTURE_FIELD_NAME, []string{data.NameImage}) modify_request.Replace(FIELD_NAME_PROFILE_PICTURE, []string{data.ProfilePicture})
} }
err = login.conn.Modify(modify_request) err = login.conn.Modify(modify_request)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 764 KiB

View file

@ -6,9 +6,6 @@
<a class="ml-auto btn btn-info" href="/">Retour</a> <a class="ml-auto btn btn-info" href="/">Retour</a>
</div> </div>
<h5>Photo de profil</h5> <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}} {{if .ErrorMessage}}
<div class="alert alert-danger mt-4">Impossible d'effectuer la modification. <div class="alert alert-danger mt-4">Impossible d'effectuer la modification.
<div style="font-size: 0.8em">{{ .ErrorMessage }}</div> <div style="font-size: 0.8em">{{ .ErrorMessage }}</div>
@ -20,44 +17,62 @@
</div> </div>
{{end}} {{end}}
<form method="POST" class="mt-4" enctype="multipart/form-data"> <form method="POST" class="mt-4" enctype="multipart/form-data">
<div class="form-group"> <div class="form-row">
<label>Nom d'utilisateur:</label> <div class="form-group col-md-6">
<input type="text" disabled="true" class="form-control" value="{{ .Status.Info.Username }}" /> <label>Nom d'utilisateur:</label>
</div> <input type="text" disabled="true" class="form-control" value="{{ .Status.Info.Username }}" />
<div class="form-group"> </div>
<label for="mail">Adresse e-mail:</label> <div class="form-group col-md-6">
<input type="text" id="mail" disabled="true" name="mail" class="form-control" value="{{ .Mail }}" /> <label for="mail">Adresse e-mail:</label>
<input type="text" id="mail" disabled="true" name="mail" class="form-control" value="{{ .Mail }}" />
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="display_name">Nom complet:</label> <label for="display_name">Nom complet:</label>
<input type="text" id="display_name" name="display_name" class="form-control" value="{{ .DisplayName }}" /> <input type="text" id="display_name" name="display_name" class="form-control" value="{{ .DisplayName }}" />
</div> </div>
<div class="form-group">
<label for="given_name">Prénom:</label> <h4>Informations complémentaires</h4>
<input type="text" id="given_name" name="given_name" class="form-control" value="{{ .GivenName }}" /> {{if .ProfilePicture}}
</div> <div class="float-right">
<div class="form-group"> <a href="/picture/{{.ProfilePicture}}">
<label for="surname">Nom de famille:</label> <img src="/picture/{{.ProfilePicture}}-thumb" />
<input type="text" id="surname" name="surname" class="form-control" value="{{ .Surname }}" /> </a>
</div> </div>
<div class="form-group"> {{end}}
<label for="description">Description (180 caractères maximum)</label>
<textarea id="description" name="description" class="form-control" maxlength="180">{{ .Description }}</textarea>
</div>
<div class="form-group form-check"> <div class="form-group form-check">
{{if .Visibility}} {{if .Visibility}}
<input class="form-check-input" name="visibility" type="checkbox" id="visibility" value="on" checked> <input class="form-check-input" name="visibility" type="checkbox" id="visibility" value="on" checked>
{{else}} {{else}}
<input class="form-check-input" name="visibility" type="checkbox" id="visibility"> <input class="form-check-input" name="visibility" type="checkbox" id="visibility">
{{end}} {{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>
<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"> <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> </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> <button type="submit" class="btn btn-primary">Enregistrer les modifications</button>
</form> </form>
<script src="/static/javascript/minio.js"></script> <script src="/static/javascript/minio.js"></script>