Add_Directory_and_ProfilePicture #9

Merged
lx merged 13 commits from Add_Directory into main 2021-08-16 14:44:53 +00:00
7 changed files with 149 additions and 140 deletions
Showing only changes of commit e94bd728ec - Show all commits

View file

@ -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"
}

View file

@ -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)

28
main.go
View file

@ -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"`
erwan marked this conversation as resolved Outdated
Outdated
Review

Endpoint de quoi? Il faudrait appeller ça S3Endpoint pour être clair, et les autres les appeller S3AccessKey et S3SecretKey.

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")
@ -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)

View file

@ -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

View file

@ -18,7 +18,7 @@ type ProfileTplData struct {
Surname string
Visibility string
Description string
NameImage 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)

Binary file not shown.

View file

@ -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,44 +17,62 @@
</div>
{{end}}
<form method="POST" class="mt-4" enctype="multipart/form-data">
<div class="form-group">
<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">
<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>
<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>
<script src="/static/javascript/minio.js"></script>