Refactoring

This commit is contained in:
Chris Mann 2023-07-26 08:31:43 +02:00
parent 065161f994
commit 4bc04f5b27
30 changed files with 3710 additions and 0 deletions

68
templates/admin.html Normal file
View file

@ -0,0 +1,68 @@
{{define "title"}}Home{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="alert alert-info">
Bienvenue, <strong>{{ .Login.Login.WelcomeName }}</strong> !
</div>
<div class="mt-3">
<div class="card">
<div class="card-header">
Mon compte
</div>
<div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action" href="/user">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>
</div>
<!--
<div class="mt-3">
<div class="card">
<div class="card-header">
Garage
</div>
<div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action" href="/garage/key">Mes identifiants</a>
<a class="list-group-item list-group-item-action" href="/garage/website">Mes sites webs</a>
</div>
</div>
</div>
-->
{{if .Common.CanInvite}}
<div class="card mt-3">
<div class="card-header">
Outils
</div>
<div class="list-group list-group-flush">
<!-- <a class="list-group-item list-group-item-action" href="/user/send_code">Envoyer un code d'invitation</a> -->
<a class="list-group-item list-group-item-action" href="/user/new">Créer un nouveau compte directement</a>
<a class="list-group-item list-group-item-action" href="/SOGo">Accéder à mes courriels, mon agenda et à mes contacts</a>
<a class="list-group-item list-group-item-action" href="https://www.lesgrandsvoisins.com/resdigita">Accéder au numérique des Grands Voisins .com</a>
</div>
</div>
{{end}}
{{if .Common.CanAdmin}}
<div class="card mt-3">
<div class="card-header">
Administration
</div>
<div class="list-group list-group-flush">
<a class="list-group-item list-group-item-action" href="/admin/activate">Activer des Utilisateur·ices</a>
<a class="list-group-item list-group-item-action" href="/admin/users">Utilisateur·ices</a>
<a class="list-group-item list-group-item-action" href="/admin/groups">Groupes</a>
<!-- <a class="list-group-item list-group-item-action" href="/admin/mailing">Mailing lists</a> -->
<a class="list-group-item list-group-item-action" href="/admin/ldap/{{.BaseDN}}">Explorateur LDAP</a>
</div>
</div>
{{end}}
{{end}}

View file

@ -0,0 +1,39 @@
{{define "title"}}Activer des utilisateurs |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<table class="table mt-4">
<thead>
<th scope="col"></th>
<th scope="col">Login</th>
<th scope="col">Email</th>
<th scope="col">Nom d'ffichage</th>
<th scope="col">Prénom</th>
<th scope="col">Nom de famille</th>
<th scope="col">description</th>
</thead>
<tbody>
{{with $root := .}}
{{range $user := $root.Users}}
<tr>
<td>
<a href="/admin/activate/{{ $user.GetAttributeValue "cn" }}">
Activer
</a>
</td>
<td>
<a href="/admin/ldap/{{$user.DN}}">{{$user.GetAttributeValue "cn"}}</a>
</td>
<td>{{$user.GetAttributeValue "mail"}}</td>
<td>{{$user.GetAttributeValue "displayName"}}</td>
<td>{{$user.GetAttributeValue "givenName"}}</td>
<td>{{$user.GetAttributeValue "sn"}}</td>
<td>{{$user.GetAttributeValue "description"}}</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
{{end}}

View file

@ -0,0 +1,96 @@
{{define "title"}}Nouvel objet |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h2>Créer un objet</h2>
</div>
<div class="mt-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{range .Path}}
<li class="breadcrumb-item"><a href="/admin/ldap/{{.DN}}">{{.Identifier}}</a></li>
{{end}}
<li class="breadcrumb-item active" aria-current="page">Nouvel objet</li>
</ol>
</nav>
</div>
{{if .Common.Error}}
<div class="alert alert-danger mt-4">Impossible de créer l'objet.
<div style="font-size: 0.8em">{{ .Common.Error }}</div>
</div>
{{end}}
<form method="POST" class="mt-4">
<!--
<div class="form-group">
<label>Parent:</label>
<input type="text" disabled="true" class="form-control" value="{{ .SuperDN }}" />
</div>
-->
{{if eq .Template "ml"}}
<div class="form-group">
<label for="idvalue">Adresse complète de la mailing list :</label>
<input type="text" id="idvalue" name="idvalue" class="form-control" value="{{ .IdValue }}" placeholder="exemple@resdigita.org" pattern="^[-A-z0-9\.]+@resdigita.org" />
</div>
{{else}}
<div class="form-group">
<label for="idvalue">Identifiant :</label>
<input type="text" id="idvalue" name="idvalue" class="form-control" value="{{ .IdValue }}" placeholder="exemple@resdigita.org" pattern="^[-A-z0-9\.]+@resdigita.org" />
</div>
{{end}}
<div class="form-group">
<label for="idtype">Type d'identifiant :</label>
<input type="text" {{if .Template}}disabled="disabled"{{end}} id="idtype" name="idtype" class="form-control" value="{{ .IdType }}" />
</div>
{{ if eq .Template "user" }}
<div class="form-group">
<label for="displayname">Nom affiché :</label>
<input type="text" id="displayname" name="displayname" class="form-control" value="{{ .DisplayName }}" />
</div>
<div class="form-group">
<label for="mail">Email:</label>
<input type="text" id="mail" name="mail" class="form-control" value="{{ .Mail }}" />
</div>
<input type="hidden" name="mail" value="" />
<div class="form-group">
<label for="givenname">Prénom :</label>
<input type="text" id="givenname" name="givenname" class="form-control" value="{{ .GivenName }}" />
</div>
<input type="hidden" name="givenname" value="" />
<div class="form-group">
<label for="sn">Nom de Famille :</label>
<input type="text" id="sn" name="sn" class="form-control" value=" {{ .SN }}" />
</div>
<input type="hidden" name="sn" value="" />
{{ else }}
<div class="form-group">
<label for="description">Description :</label>
<input type="text" id="description" name="description" class="form-control" value="{{ .Description }}" />
</div>
<input type="hidden" name="description" value="" />
<div class="form-group">
<label for="member">Member :</label>
<input type="text" id="member" name="member" class="form-control" value="{{ .Member }}" />
</div>
<input type="hidden" name="member" value="" />
{{ end }}
<!--
<div class="form-group">
<label for="soc">StructuralObjectClass :</label>
<input type="text" {{if .Template}}disabled="disabled"{{end}} id="soc" name="soc" class="form-control" value="{{ .StructuralObjectClass }}" />
</div>
-->
<div class="form-group">
<label for="oc">ObjectClass :</label>
<textarea rows="5" {{if .Template}}disabled="disabled"{{end}} id="oc" name="oc" class="form-control">{{ .ObjectClass }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Créer l'objet</button>
</form>
{{end}}

View file

@ -0,0 +1,40 @@
{{define "title"}}Liste des groupes |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h2>Liste des groupes</h2>
<a class="ml-auto btn btn-success" href="/admin/create/group/{{.GroupBaseDN}}">Nouveau groupe</a>
</div>
<div class="alert alert-warning mt-4">
Les groupes servent uniquement à contrôler l'accès à différentes fonctionalités de Deuxfleurs.
Ce ne sont pas des <a href="/admin/mailing">mailing lists</a>.
</div>
<table class="table mt-4">
<thead>
<th scope="col">Identifiant</th>
<th scope="col">Nom complet</th>
</thead>
<tbody>
{{with $root := .}}
{{range $group := $root.Groups}}
<tr>
<td>
<a href="/admin/ldap/{{$group.DN}}">
{{$group.GetAttributeValue $root.GroupNameAttr}}
</a>
</td>
<td>{{$group.GetAttributeValue "description"}}</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
{{end}}

239
templates/admin/ldap.html Normal file
View file

@ -0,0 +1,239 @@
{{define "title"}}Explorateur LDAP |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h2>Explorateur LDAP</h2>
</div>
<div class="mt-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{range .Path}}
{{if .Active}}
<li class="breadcrumb-item active" aria-current="page">{{.Identifier}}</li>
{{else}}
<li class="breadcrumb-item"><a href="/admin/ldap/{{.DN}}">{{.Identifier}}</a></li>
{{end}}
{{end}}
</ol>
</nav>
</div>
<table class="table mt-4">
<tbody>
{{range .ChildrenOU}}
<tr>
<td>
<a href="/admin/ldap/{{.DN}}">
🗀 {{.Identifier}}
</a>
</td>
<td>{{.Name}}</td>
</tr>
{{end}}
{{range .ChildrenOther}}
<tr>
<td>
<a href="/admin/ldap/{{.DN}}">
{{.Identifier}}
</a>
</td>
<td>{{.Name}}</td>
</tr>
{{end}}
</tbody>
</table>
{{if .CanAddChild}}
<div class="mt-2">
<a class="btn btn-sm btn-success" href="/admin/create/user/{{.DN}}">+utilisateur</a>
<a class="ml-4 btn btn-sm btn-success" href="/admin/create/group/{{.DN}}">+groupe</a>
<a class="ml-4 btn btn-sm btn-success" href="/admin/create/ou/{{.DN}}">+ou</a>
<a class="ml-4 btn btn-sm btn-success" href="/admin/create/generic/{{.DN}}">+objet</a>
</div>
<hr class="mt-4" />
{{end}}
{{if .Common.Success}}
<div class="alert alert-success mt-2">Modification enregistrée.</div>
{{end}}
{{if .Common.Error}}
<div class="alert alert-danger mt-2">
Impossible d'effectuer la modification.
<div style="font-size: 0.8em">{{.Error}}</div>
</div>
{{end}}
<h5>Attributs</h5>
<div class="container">
{{range $key, $value := .Props}}
{{if $value.Editable}}
<div class="row mt-4">
<div class="col-md-3"><strong>{{$value.Name}}</strong></div>
<div class="col-md-7">
<form method="POST">
<div class="form-row">
<input type="hidden" name="action" value="modify" />
<input type="hidden" name="attr" value="{{$key}}" />
<textarea name="values" rows="{{len $value.Values}}" class="form-control col-md-9">{{range $i, $x := $value.Values}}{{if $i}}{{"\n"}}{{end}}{{$x}}{{end}}</textarea>
<div class="col-md-3">
<input type="submit" value="Modifier" class="form-control btn btn-primary" />
</div>
</div>
</form>
</div>
<div class="col-md-1">
{{if $value.Deletable}}
<form method="POST" onsubmit="return confirm('Supprimer cet attribut ?');">
<input type="hidden" name="action" value="delete" />
<input type="hidden" name="attr" value="{{$key}}" />
<input type="submit" value="Suppr." class="form-control btn btn-danger btn-sm" />
</form>
{{end}}
</div>
</div>
{{end}}
{{end}}
{{range $key, $value := .Props}}
{{if not $value.Editable}}
<div class="row mt-4">
<div class="col-md-3"><strong>{{$key}}</strong></div>
<div class="col-md-9">
{{range $value.Values}}
{{if eq $key "creatorsname" "modifiersname" }}
<div><a href="/admin/ldap/{{.}}">{{.}}</a></div>
{{else}}
<div>{{.}}</div>
{{end}}
{{end}}
</div>
</div>
{{end}}
{{end}}
<form method="POST">
<div class="row mt-4">
<div class="col-md-3">
<input type="hidden" name="action" value="add" />
<input class="form-control" type="text" name="attr" placeholder="Ajouter un attribut..." />
</div>
<div class="col-md-7">
<div class="form-row">
<textarea name="values" placeholder="Valeur(s)..." rows="2" class="form-control col-md-9"></textarea>
<div class="col-md-3">
<input type="submit" value="Ajouter" class="form-control btn btn-success" />
</div>
</div>
</div>
</div>
</form>
</div>
{{if .HasMembers}}
<hr class="mt-4" />
<h5 class="mt-4">Membres</h5>
<div class="container">
{{range .Members}}
<div class="row mt-4">
<div class="col-md-3">
<strong>{{.Name}}</strong>
</div>
<div class="col-md-5">
<a href="/admin/ldap/{{.DN}}">{{.DN}}</a>
</div>
<div class="col-md-2">
<form method="POST" onsubmit="return confirm('Supprimer du groupe ?');">
<input type="hidden" name="action" value="delete-member" />
<input type="hidden" name="member" value="{{.DN}}" />
<input type="submit" value="Supprimer" class="form-control btn btn-danger btn-sm" />
</form>
</div>
</div>
{{end}}
<form method="POST">
<input type="hidden" name="action" value="add" />
<input type="hidden" name="attr" value="member" />
<div class="row mt-4">
<div class="col-md-3"><strong>Ajouter au groupe :</strong>
</div>
<div class="col-md-5">
<input class="form-control" type="text" list="users" name="values" placeholder="Utilisateur..." />
<datalist id="users">
{{range .PossibleNewMembers}}
<option value="{{.DN}}">{{.Name}}</option>
{{end}}
</datalist>
</div>
<div class="col-md-2">
<input type="submit" value="Ajouter" class="form-control btn btn-success btn-sm" />
</div>
</form>
</div>
{{end}}
{{if .HasGroups}}
<hr class="mt-4" />
<h5 class="mt-4">Membre de</h5>
<div class="container">
{{range .Groups}}
<div class="row mt-4">
<div class="col-md-3">
<strong>{{.Name}}</strong>
</div>
<div class="col-md-5">
<a href="/admin/ldap/{{.DN}}">{{.DN}}</a>
</div>
<div class="col-md-2">
<form method="POST" onsubmit="return confirm('Supprimer du groupe ?');">
<input type="hidden" name="action" value="delete-from-group" />
<input type="hidden" name="group" value="{{.DN}}" />
<input type="submit" value="Supprimer" class="form-control btn btn-danger btn-sm" />
</form>
</div>
</div>
{{end}}
<form method="POST">
<input type="hidden" name="action" value="add-to-group" />
<div class="row mt-4">
<div class="col-md-3"><strong>Nouveau groupe :</strong>
</div>
<div class="col-md-5">
<input class="form-control" list="groups" type="text" name="group" placeholder="Groupe..." />
<datalist id="groups">
{{range .PossibleNewGroups}}
<option value="{{.DN}}">{{.Name}}</option>
{{end}}
</datalist>
</div>
<div class="col-md-2">
<input type="submit" value="Ajouter" class="form-control btn btn-success btn-sm" />
</div>
</form>
</div>
{{end}}
{{if .CanDelete}}
<hr class="mt-4">
<h5 class="mt-4">Supprimer l'objet</h5>
<div class="alert alert-danger">
Attention, cette opération est irrévocable !
</div>
<form method="POST" onsubmit="return confirm('Supprimer cet objet DÉFINITIVEMENT ?');">
<div class="form-row">
<input type="hidden" name="action" value="delete-object" />
<div class="col-sm-5"></div>
<input type="submit" value="Supprimer l'objet" class="form-control btn btn-danger col-sm-2" />
<div class="col-sm-5"></div>
</div>
</form>
{{end}}
<hr class="mt-4" />
{{end}}

View file

@ -0,0 +1,35 @@
{{define "title"}}Mailing lists |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h4>Mailing lists</h4>
<a class="ml-auto btn btn-success" href="/admin/create/ml/{{.MailingBaseDN}}">Nouvelle mailing list</a>
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
</div>
<table class="table mt-4">
<thead>
<th scope="col">Adresse</th>
<th scope="col">Description</th>
</thead>
<tbody>
{{with $root := .}}
{{range $ml := $root.MailingLists}}
<tr>
<td>
<a href="/admin/mailing/{{$ml.GetAttributeValue $root.MailingNameAttr}}">
{{$ml.GetAttributeValue $root.MailingNameAttr}}
</a>
</td>
<td>{{$ml.GetAttributeValue "description"}}</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
{{end}}

View file

@ -0,0 +1,117 @@
{{define "title"}}ML {{.MailingList.GetAttributeValue .MailingNameAttr}} |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h2>ML {{.MailingList.GetAttributeValue .MailingNameAttr}}
<a class="ml-auto btn btn-sm btn-dark" href="/admin/ldap/{{.MailingList.DN}}">Vue avancée</a>
</h2>
<a class="ml-auto btn btn-dark" href="/admin/mailing">Liste des ML</a>
</div>
{{if .Common.Success}}
<div class="alert alert-success mt-2">Modification enregistrée.</div>
{{end}}
{{if .Common.Error}}
<div class="alert alert-danger mt-2">
Impossible d'effectuer la modification.
<div style="font-size: 0.8em">{{.Error}}</div>
</div>
{{end}}
{{with $desc := .MailingList.GetAttributeValue "description"}}{{if $desc}}
<p class="mt-4">{{$desc}}</p>
{{end}}{{end}}
<table class="table mt-4">
<thead>
<th scope="col">Adresse</th>
<th scope="col">Nom</th>
<th scope="col" style="width: 6em"></th>
</thead>
<tbody>
{{with $root := .}}
{{range $member := $root.Members}}
<tr>
<td>
<a href="/admin/ldap/{{$member.DN}}">
{{$member.GetAttributeValue "mail"}}
</a>
</td>
<td>{{$member.GetAttributeValue "displayname"}}</td>
<td>
<form method="POST" onsubmit="return confirm('Supprimer de la ML ?');">
<input type="hidden" name="action" value="delete-member" />
<input type="hidden" name="member" value="{{.DN}}" />
<input type="submit" value="Suppr" class="form-control btn btn-danger btn-sm" />
</form>
</td>
</tr>
{{end}}
{{end}}
{{if not .Members}}
<tr><td>(aucun abonné)</td></tr>
{{end}}
</tbody>
</table>
<hr class="mt-4" />
<h5 class="mt-4">Ajouter un destinataire</h5>
<div class="container">
<form method="POST">
<input type="hidden" name="action" value="add-member" />
<div class="row mt-4">
<div class="col-md-3"><strong>Utilisateur existant :</strong> </div>
<div class="col-md-5">
<input class="form-control" type="text" list="users" name="member" placeholder="Utilisateur..." />
<datalist id="users">
{{range .PossibleNewMembers}}
{{if .GetAttributeValue "mail"}}
<option value="{{.DN}}">{{if .GetAttributeValue "displayname"}}{{.GetAttributeValue "displayname"}} ({{.GetAttributeValue "mail" }}){{else}}{{.GetAttributeValue "mail"}}{{end}}</option>
{{end}}
{{end}}
</datalist>
</div>
<div class="col-md-2">
<input type="submit" value="Ajouter" class="form-control btn btn-success btn-sm" />
</div>
</div>
</form>
{{if .AllowGuest}}
<div class="row mt-4">
<div class="col-md-10">OU</div>
</div>
<form method="POST">
<input type="hidden" name="action" value="add-external" />
<div class="row mt-4">
<div class="col-md-3"><strong>E-mail :</strong></div>
<div class="col-md-5">
<input class="form-control" type="text" name="mail" placeholder="machin@truc.net..." />
</div>
<div class="col-md-2">
</div>
</div>
<div class="row mt-4">
<div class="col-md-3"><strong>Nom (optionnel) :</strong></div>
<div class="col-md-5">
<input class="form-control" type="text" name="displayname" placeholder="Machin Truc..." />
</div>
<div class="col-md-2">
<input type="submit" value="Ajouter" class="form-control btn btn-success btn-sm" />
</div>
</div>
<div class="row">
<small class="form-text text-muted col-md-10">
Si un utilisateur existe déjà avec l'email spécifiée, celui-ci sera ajouté à la liste.
Sinon, un utilisateur invité sera créé.
</small>
</div>
</form>
{{end}}
</div>
{{end}}

View file

@ -0,0 +1,43 @@
{{define "title"}}Liste des utilisateurs |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h2>Liste des utilisateurs</h2>
<a class="ml-auto btn btn-success" href="/admin/create/user/{{.UserBaseDN}}">Nouvel utilisateur</a>
</div>
<table class="table mt-4">
<thead>
<th scope="col"></th>
<th scope="col">Identifiant</th>
<th scope="col">Nom complet</th>
<th scope="col">Email</th>
</thead>
<tbody>
{{with $root := .}}
{{range $user := $root.Users}}
<tr>
<td>
<a href="/admin/unactivate/{{ $user.GetAttributeValue "cn" }}">
Dèsactiver
</a>
</td>
<td>
<a href="/admin/ldap/{{$user.DN}}">
{{$user.GetAttributeValue $root.UserNameAttr}}
</a>
</td>
<td>{{$user.GetAttributeValue "displayName"}}</td>
<td>{{$user.GetAttributeValue "mail"}}</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
{{end}}

View 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}}

237
templates/garage/key.html Normal file
View file

@ -0,0 +1,237 @@
{{define "title"}}Profile |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h4>Mes identifiants</h4>
<a class="ml-auto btn btn-link" href="/garage/website">Mes sites webs</a>
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
</div>
<ul class="nav nav-tabs" id="proto" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="s3-tab" data-toggle="tab" href="#s3" role="tab" aria-controls="s3" aria-selected="true">S3</a>
</li>
<li class="nav-item">
<a class="nav-link" id="sftp-tab" data-toggle="tab" href="#sftp" role="tab" aria-controls="sftp" aria-selected="false">SFTP</a>
</li>
</ul>
<div class="tab-content" id="protocols">
<div class="tab-pane fade show active" id="s3" role="tabpanel" aria-labelledby="s3-tab">
<table class="table mt-4">
<tbody>
<tr>
<th scope="row">Identifiant de clé</th>
<td>{{ .Key.AccessKeyId }}</td>
</tr>
<tr>
<th scope="row">Clé secrète</th>
<td>{{ .Key.SecretAccessKey }}</td>
</tr>
<tr>
<th scope="row">Région</th>
<td>garage</td>
</tr>
<tr>
<th scope="row">Endpoint URL</th>
<td>https://garage.resdigita.org</td>
</tr>
<tr>
<th scope="row">Type d'URL</th>
<td>DNS et chemin (préférer chemin)</td>
</tr>
<tr>
<th scope="row">Signature</th>
<td>Version 4</td>
</tr>
</tbody>
</table>
<p>Configurer votre logiciel :</p>
<div class="accordion" id="softconfig">
<div class="card">
<div class="card-header" id="awscli-title">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#awscli" aria-expanded="false" aria-controls="awscli">
awscli
</button>
</h2>
</div>
<div id="awscli" class="collapse show" aria-labelledby="awscli-title" data-parent="#softconfig">
<div class="card-body">
<p>Créez un fichier nommé <code>~/.awsrc</code> :</p>
<pre>
export AWS_ACCESS_KEY_ID={{ .Key.AccessKeyId }}
export AWS_SECRET_ACCESS_KEY={{ .Key.SecretAccessKey }}
export AWS_DEFAULT_REGION='garage'
function aws { command aws --endpoint-url https://garage.resdigita.org $@ ; }
aws --version
</pre>
<p>Ensuite vous pouvez utiliser awscli :</p>
<pre>
source ~/.awsrc
aws s3 ls
aws s3 ls s3://my-bucket
aws s3 cp /tmp/a.txt s3://my-bucket
...
</pre>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="minio-title">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#minio" aria-expanded="true" aria-controls="minio">
Minio CLI
</button>
</h2>
</div>
<div id="minio" class="collapse" aria-labelledby="minio-title" data-parent="#softconfig">
<div class="card-body">
<p>Vous pouvez configurer Minio CLI avec cette commande :</p>
<pre>
mc alias set \
garage \
https://garage.resdigita.org \
{{ .Key.AccessKeyId }} \
{{ .Key.SecretAccessKey }} \
--api S3v4
</pre>
<p>Et ensuite pour utiliser Minio CLI avec :</p>
<pre>
mc ls garage/
mc cp /tmp/a.txt garage/my-bucket/a.txt
...
</pre>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="winscp-title">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#winscp" aria-expanded="true" aria-controls="winscp">
WinSCP
</button>
</h2>
</div>
<div id="winscp" class="collapse" aria-labelledby="winscp-title" data-parent="#softconfig">
<div class="card-body">
Reportez vous <a href="">au guide</a>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="hugo-title">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#hugo" aria-expanded="false" aria-controls="hugo">
Hugo
</button>
</h2>
</div>
<div id="hugo" class="collapse" aria-labelledby="hugo-title" data-parent="#softconfig">
<div class="card-body">
<p>Dans votre fichier <code>config.toml</code>, rajoutez :</p>
<pre>
[[deployment.targets]]
URL = "s3://bucket?endpoint=garage.resdigita.org&amp;s3ForcePathStyle=true&amp;region=garage"
</pre>
<p>Assurez-vous d'avoir un fichier dans lequel les variables <code>AWS_ACCESS_KEY_ID</code> et <code>AWS_SECRET_ACCESS_KEY</code> sont définies,
ici on suppose que vous avez suivi les instructions de l'outil awscli (ci-dessus) et que vous avez un fichier <code>~/.awsrc</code> qui défini ces variables.
Ensuite : </p>
<pre>
source ~/.awsrc
hugo deploy
</pre>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="publii-title">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#publii" aria-expanded="false" aria-controls="publii">
Publii
</button>
</h2>
</div>
<div id="publii" class="collapse" aria-labelledby="publii-title" data-parent="#softconfig">
<div class="card-body">
<em>Bientôt...</em>
</div>
</div>
</div>
</div>
</div>
<!-- sftp -->
<div class="tab-pane fade" id="sftp" role="tabpanel" aria-labelledby="sftp-tab">
<table class="table mt-4">
<tbody>
<tr>
<th scope="row">Nom d'utilisateur-ice</th>
<td>{{ .Login.Status.Info.Username }}</td>
</tr>
<tr>
<th scope="row">Mot de passe</th>
<td>(votre mot de passe guichet)</td>
</tr>
<tr>
<th scope="row">Hôte</th>
<td>sftp://bagage.resdigita.org</td>
</tr>
<tr>
<th scope="row">Port</th>
<td>2222</td>
</tr>
</tbody>
</table>
<p>Configurer votre logiciel :</p>
<div class="accordion" id="softconfig2">
<div class="card">
<div class="card-header" id="filezilla-title">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#filezilla" aria-expanded="false" aria-controls="filezilla">
scp
</button>
</h2>
</div>
<div id="filezilla" class="collapse show" aria-labelledby="filezilla-title" data-parent="#softconfig">
<div class="card-body">
<p>Un exemple avec SCP :</p>
<pre>
scp -oHostKeyAlgorithms=+ssh-rsa -P2222 -r ./public {{ .Login.Status.Info.Username }}@bagage.resdigita.org:mon_bucket/
</pre>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="filezilla-title">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#filezilla" aria-expanded="false" aria-controls="filezilla">
Filezilla
</button>
</h2>
</div>
<div id="filezilla" class="collapse" aria-labelledby="filezilla-title" data-parent="#softconfig">
<div class="card-body">
<em>Bientôt</em>
</div>
</div>
</div>
</div>
</div>
</div>
{{end}}

View file

@ -0,0 +1,61 @@
{{define "title"}}Inspecter le site web |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h4>Inspecter le site web</h4>
<a class="ml-auto btn btn-link" href="/garage/key">Mes identifiants</a>
<a class="ml-4 btn btn-success" href="/garage/website/new">Nouveau site web</a>
<a class="ml-4 btn btn-info" href="/garage/website">Mes sites webs</a>
</div>
<table class="table mt-4">
<tbody>
<tr>
<th scope="row">ID</th>
<td>{{ .Bucket.Id }}</td>
</tr>
<tr>
<th scope="row">URLs</th>
<td>
{{ range $alias := .Bucket.GlobalAliases }}
{{ if contains $alias "." }}
https://{{ $alias }}
{{ else }}
https://{{ $alias }}.web.resdigita.org
{{ end }}
{{ end }}
</td>
</tr>
<tr>
<th scope="row">Document d'index</th>
<td> {{ .IndexDoc }}</td>
</tr>
<tr>
<th scope="row">Document d'erreur</th>
<td>{{ .Common.ErrorDoc }}</td>
</tr>
<tr>
<th scope="row">Nombre de fichiers</th>
<td>{{ .Bucket.Objects }} / {{ .MaxObjects }}</td>
</tr>
<tr>
<th scope="row">Espace utilisé</th>
<td>{{ .Bucket.Bytes }} / {{ .MaxSize }} octets</td>
</tr>
</tbody>
</table>
<h4>Configurer le nom de domaine</h4>
{{ range $alias := .Bucket.GlobalAliases }}
{{ if contains $alias "." }}
<p> Le nom de domaine {{ $alias }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée <code>CNAME garage.resdigita.org</code> ou <code>ALIAS garage.resdigita.org</code> auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).</p>
{{ else }}
<p> Le nom de domaine https://{{ $alias }}.web.resdigita.org est fourni par Deuxfleurs, il n'y a pas de configuration à faire.</p>
{{ end }}
{{ end }}
{{end}}

View file

@ -0,0 +1,41 @@
{{define "title"}}Sites webs |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h4>Sites webs</h4>
<a class="ml-auto btn btn-link" href="/garage/key">Mes identifiants</a>
<a class="ml-4 btn btn-success" href="/garage/website/new">Nouveau site web</a>
<a class="ml-4 btn btn-info" href="/">Menu principal</a>
</div>
<table class="table mt-4">
<thead>
<th scope="col">ID</th>
<th scope="col">URLs</th>
</thead>
<tbody>
{{ range $buck := .Key.Buckets }}
{{ if $buck.GlobalAliases }}
<tr>
<td>
<a href="/garage/website/b/{{$buck.Id}}">{{$buck.Id}}</a>
</td>
<td>
{{ range $alias := $buck.GlobalAliases }}
{{ if contains $alias "." }}
https://{{ $alias }}
{{ else }}
https://{{ $alias }}.web.resdigita.org
{{ end }}
{{ end }}
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
{{end}}

View file

@ -0,0 +1,66 @@
{{define "title"}}Créer un site web |{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h4>Créer un site web</h4>
<a class="ml-auto btn btn-link" href="/garage/key">Mes identifiants</a>
<a class="ml-4 btn btn-info" href="/garage/website">Mes sites webs</a>
</div>
<ul class="nav nav-tabs" id="proto" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="dnsint-tab" data-toggle="tab" href="#dnsint" role="tab" aria-controls="dnsint" aria-selected="true">Je n'ai pas de nom de domaine</a>
</li>
<li class="nav-item">
<a class="nav-link" id="dnsext-tab" data-toggle="tab" href="#dnsext" role="tab" aria-controls="dnsext" aria-selected="false">Utiliser mon propre nom de domaine</a>
</li>
</ul>
<div class="tab-content" id="protocols">
<div class="tab-pane fade show active" id="dnsint" role="tabpanel" aria-labelledby="dnsint-tab">
<form method="POST" class="mt-4">
<div class="form-row">
<div class="form-group col-md-6">
<label for="bucket">Sous-domaine désiré :</label>
<input type="text" id="bucket" name="bucket" placeholder="mon-site" class="form-control" value="" onkeyup="document.getElementById('url').value = `https://${document.getElementById('bucket').value}.web.resdigita.org`" />
</div>
<div class="form-group col-md-6">
<label for="url">Votre site sera accessible à l'URL suivante :</label>
<input type="text" id="url" disabled="true" name="url" class="form-control" value="https://mon-site.web.resdigita.org" />
</div>
</div>
<div class="mt-4">
<p>La première fois que vous chargerez votre site web, une erreur de certificat sera renvoyée. C'est normal, il faudra patienter quelques minutes le temps que le certificat se génère.</p>
</div>
<button type="submit" class="btn btn-primary">Créer un nouveau site web</button>
</form>
</div>
<div class="tab-pane fade show" id="dnsext" role="tabpanel" aria-labelledby="dnsext-tab">
<form method="POST" class="mt-4">
<div class="form-row">
<div class="form-group col-md-6">
<label for="bucket2">Votre nom de domaine :</label>
<input type="text" id="bucket2" name="bucket2" placeholder="exemple.com" class="form-control" value="" onkeyup="document.getElementById('url2').value = `https://${document.getElementById('bucket2').value}`" />
</div>
<div class="form-group col-md-6">
<label for="url2">Votre site sera accessible à l'URL suivante :</label>
<input type="text" id="url2" disabled="true" name="url2" class="form-control" value="https://exemple.com" />
</div>
</div>
<div>
<p>Vous devez éditer votre zone DNS, souvent gérée par votre bureau d'enregistrement, comme Gandi, pour la faire pointer vers Deuxfleurs. Si vous utilisez un sous domaine (eg. <code>site.exemple.com</code>), une entrée <code>CNAME</code> est appropriée :</p>
<pre>site CNAME 3600 garage.resdigita.org.</pre>
<p>Si vous utilisez la racine de votre nom de domaine (eg. <code>exemple.com</code>, aussi appelée APEX), la solution dépend de votre fournisseur DNS, il vous faudra au choix une entrée <code>ALIAS</code> ou <code>CNAME</code> en fonction de ce que votre fournisseur supporte :</p>
<pre>@ ALIAS 3600 garage.resdigita.org.</pre>
<p>La première fois que vous chargerez votre site web, une erreur de certificat sera renvoyée. C'est normal, il faudra patienter quelques minutes le temps que le certificat se génère.</p>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">Créer un nouveau site web</button>
</form>
</div>
</div>
{{end}}

View file

@ -0,0 +1,40 @@
{{define "title"}}G Pas (Je n'ai pas mon mal de passe){{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<h2>G Pas</h2>
<p>Refaire son mot de passe</p>
{{if .Common.ErrorMessage}}
<div class="alert alert-danger">Impossible
<div style="font-size: 0.8em">{{ .Common.ErrorMessage }}</div>
</div>
{{end}}
{{if .Common.Success}}
<div class="alert alert-success mt-4">
Email envoyé au courriel de secours.
</div>
{{end}}
<p>Merci de renseigner au moins un des champs ci-dessous.</p>
<form method="POST">
<div class="form-group">
<label for="username">Ou identifiant :</label>
<input type="text" name="username" id="username" class="form-control" value="{{ .Login.Username }}" />
</div>
<div class="form-group">
<label for="mail">ou mail (interne aux GV) :</label>
<input type="text" name="mail" id="mail" class="form-control" value="{{ .Mail }}" />
</div>
<div class="form-group">
<label for="othermailbox">ou mail de secours :</label>
<input type="text" name="othermailbox" id="othermailbox" class="form-control" value="{{ .OtherMailbox }}" />
</div>
<button type="submit" class="btn btn-primary">Refaire son mal de passe</button>
</form>
<script src="/static/javascript/minio.js"></script>
{{end}}

View file

@ -0,0 +1,13 @@
From: {{.From}}
To: {{.To}}
Subject: Code d'invitation GVoisin.com
Content-type: text/plain; charset=utf-8
Une refonte de mot de passe avait été demandé sur GVoisin.com
Pour créer votre compte, rendez-vous à l'adresse suivante:
{{.WebBaseAddress}}/passwd/lost/{{.Code}}
À bientôt sur GVoisin.com !

86
templates/user.html Normal file
View file

@ -0,0 +1,86 @@
{{define "title"}}Profile{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h2>Modifier mon profil</h2>
</div>
{{if .Common.ErrorMessage}}
<div class="alert alert-danger mt-4">Impossible d'effectuer la modification.
<div style="font-size: 0.8em">{{ .Common.ErrorMessage }}</div>
</div>
{{end}}
{{if .Common.Success}}
<div class="alert alert-success mt-4">
Profil enregistré.
</div>
{{end}}
<form method="POST" class="mt-4" enctype="multipart/form-data">
<div class="form-row">
<div class="form-group col-md-6">
<label>Identifiant:</label>
<input type="text" disabled="true" class="form-control" value="{{ .Login.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>
{{/*
<!--
<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="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}}

View file

@ -0,0 +1,4 @@
{{define "title"}}Créer un compte |{{end}}
{{define "admenu"}}
{{end}}

View file

@ -0,0 +1,66 @@
{{define "title"}}Envoyer un code d'invitation{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h4>Envoyer un code d'invitation</h4>
<a class="ml-auto btn btn-info" href="/">Retour</a>
</div>
{{if .Common.ErrorMessage}}
<div class="alert alert-danger mt-4">Impossible de génerer ou d'envoyer le code.
<div style="font-size: 0.8em">{{ .Common.ErrorMessage }}</div>
</div>
{{end}}
{{if .Common.Success}}
<div class="alert alert-success mt-4">
{{if .CodeSentTo}}
Un code d'invitation a bien été envoyé à <code>{{ .CodeSentTo }}</code>.
{{end}}
{{if .CodeDisplay}}
Lien d'invitation :
<p style="text-align: center; font-size: 1.4em" class="mt-4 mb-4">
<a href="{{.WebBaseAddress}}/invitation/{{ .CodeDisplay }}">{{.WebBaseAddress}}/invitation/{{.CodeDisplay}}</a>
</p>
{{end}}
</div>
{{else}}
<form method="POST" class="mt-4">
Choisissez une option:
<div class="input-group mt-4">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="radio" name="choice" value="send" id="choice_send" checked="true">
</div>
</div>
<label class="form-control" for="choice_send">
Envoyer le code à l'addresse suivante:
</label>
<input class="form-control" type="text" name="sendto" id="sendto" placeholder="Addresse mail..." onclick="document.getElementById('choice_send').checked = true;" />
</div>
{{if .Common.ErrorInvalidEmail}}
<div class="alert alert-warning mt-4">
Addresse mail invalide.
</div>
{{end}}
<div class="input-group mt-4">
<div class="input-group-prepend">
<div class="input-group-text">
<input type="radio" name="choice" value="display" id="choice_display">
</div>
</div>
<label class="form-control" for="choice_display">
Afficher le code et me laisser l'envoyer
</label>
</div>
<div class="form-group mt-4">
<button type="submit" class="btn btn-primary">Génerer le code</button>
</div>
</form>
{{end}}
{{end}}

13
templates/user/mail.txt Normal file
View file

@ -0,0 +1,13 @@
From: {{.From}}
To: {{.To}}
Subject: Code d'invitation GVoisin.com
Content-type: text/plain; charset=utf-8
Vous avez été invité à créer un compte sur GVoisin.com par {{.InviteFrom}} :)
Pour créer votre compte, rendez-vous à l'addresse suivante:
{{.WebBaseAddress}}/invitation/{{.Code}}
À bientôt sur GVoisin.com !

131
templates/user/new.html Normal file
View file

@ -0,0 +1,131 @@
{{define "title"}}Créer un compte{{end}}
{{define "admenu"}}
{{end}}
{{define "body"}}
<div class="d-flex">
<h2>Création d'un nouveau compte</h2>
</div>
{{if .Common.ErrorMessage}}
<div class="alert alert-danger mt-4">Impossible de créer le compte.
<div style="font-size: 0.8em">{{ .Common.ErrorMessage }}</div>
</div>
{{end}}
{{if .Common.WarningMessage}}
<div class="alert alert-danger mt-4">Des erreurs se sont produites, le compte pourrait ne pas être totalement
fonctionnel.
<div style="font-size: 0.8em">{{ .Common.WarningMessage }}</div>
</div>
{{end}}
{{if .Common.Success}}
<div class="alert alert-success mt-4">
Le compe a été créé !
Rendez-vous <a href="/session/logout">sur la page d'accueil</a> pour vous connecter avec ce nouveau compte.
</div>
{{else}}
<form method="POST" class="mt-4">
<div class="form-group">
<label for="givenname">Prénom :</label>
<input type="text" id="givenname" name="givenname" 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="otheremail">Email de secours:</label>
<input type="text" id="otheremail" name="otheremail" class="form-control" value="{{ .OtherEmail }}" />
<small class="form-text text-muted">
Le courriel de l'utilisateur.
</small>
</div>
<div class="form-group">
<label for="displayname">Nom affiché :</label>
<input type="text" id="displayname" name="displayname" class="form-control" value="{{ .DisplayName }}" />
</div>
<div class="form-group">
<label for="username">Identifiant :</label>
<input type="text" id="username" name="username" class="form-control" />
<small class="form-text text-muted">
Votre identifiant doit être en minuscule.
</small>
</div>
<div id="calc-uid"></div>
<div id="calc-cn"></div>
{{if .ErrorInvalidUsername}}
<div class="alert alert-warning">
Nom d'utilisateur invalide. Ne peut contenir que les caractères suivants : chiffres, lettres minuscules, point,
tiret bas (_) et tiret du milieu (-).
</div>
{{end}}
{{if .ErrorUsernameTaken}}
<div class="alert alert-warning">
Ce nom d'utilisateur est déjà pris.
</div>
{{end}}
<div class="form-group">
<label for="mail">Email des GV:</label>
<input type="text" id="mail" name="mail" class="form-control" value="{{ .Mail }}" />
<small class="form-text text-muted">
Le courriel et login interne.
</small>
</div>
<p><a href="#" onClick="javascript:var suggestPW = Math.random().toString(36).slice(-10); document.getElementById('password').value='{{ .SuggestPW }}';document.getElementById('password2').value='{{ .SuggestPW }}';">Utiliser ce mot de passe :</a> {{ .SuggestPW }}</p>
<div class="form-group">
<label for="password">Mot de passe :</label>
<input type="password" id="password" name="password" class="form-control" />
<small class="form-text text-muted">
La seule contrainte est que votre mot de passe doit faire au moins 8 caractères. Utilisez chiffres, majuscules, et
caractères spéciaux sans modération !
</small>
</div>
{{if .ErrorPasswordTooShort}}
<div class="alert alert-warning">
Le mot de passe choisi est trop court (minimum 8 caractères).
</div>
{{end}}
<div class="form-group">
<label for="password2">Répéter le mot de passe :</label>
<input type="password" id="password2" name="password2" class="form-control" />
</div>
{{if .ErrorPasswordMismatch}}
<div class="alert alert-warning">
Les deux mots de passe entrés ne correspondent pas.
</div>
{{end}}
<button type="submit" class="btn btn-primary">Créer le compte</button>
</form>
<script lang="javascript">
function changeGivenNameOrSurname () {
givenname = document.getElementById("givenname");
surname = document.getElementById("surname");
displayname = document.getElementById("displayname");
username = document.getElementById("username");
displayname.value = givenname.value + " " + surname.value
changeDisplayname();
}
function changeDisplayname () {
givenname = document.getElementById("givenname");
surname = document.getElementById("surname");
displayname = document.getElementById("displayname");
username = document.getElementById("username");
username.value = displayname.value.toLowerCase().replace(/[^A-z0-9.-]/g, '').replace(/^[.-]/, '').replace(/[.-]$/, '');
changeUsername();
}
function changeUsername () {
givenname = document.getElementById("givenname");
surname = document.getElementById("surname");
displayname = document.getElementById("displayname");
username = document.getElementById("username");
mail = document.getElementById("mail");
mail.value = username.value + "@lesgv.com";
}
document.getElementById("givenname").addEventListener("change",changeGivenNameOrSurname);
document.getElementById("surname").addEventListener("change",changeGivenNameOrSurname);
document.getElementById("displayname").addEventListener("change",changeDisplayname);
document.getElementById("username").addEventListener("change",changeUsername);
</script>
{{end}}
{{end}}

53
utils-config.go Normal file
View file

@ -0,0 +1,53 @@
/*
config handles reading the config.json file at the root and processing the settings
*/
package main
import (
"encoding/json"
"flag"
"io/ioutil"
"log"
"os"
)
var configFlag = flag.String("config", "./config.json", "Configuration file path")
var config *ConfigFile
func readConfig() ConfigFile {
// Default configuration values for certain fields
config_file := ConfigFile{
HttpBindAddr: ":9991",
LdapServerAddr: "ldap://127.0.0.1:389",
UserNameAttr: "uid",
GroupNameAttr: "gid",
InvitationNameAttr: "cn",
InvitedAutoGroups: []string{},
Org: "ResDigita",
}
_, err := os.Stat(*configFlag)
if os.IsNotExist(err) {
log.Fatalf("Could not find Guichet configuration file at %s. Please create this file, for exemple starting with config.json.exemple and customizing it for your deployment.", *configFlag)
}
if err != nil {
log.Fatal(err)
}
bytes, err := ioutil.ReadFile(*configFlag)
if err != nil {
log.Fatal(err)
}
err = json.Unmarshal(bytes, &config_file)
if err != nil {
log.Fatal(err)
}
return config_file
}

40
utils-http.go Normal file
View file

@ -0,0 +1,40 @@
/*
http-utils provide utility functions that interact with http
*/
package main
import (
"crypto/tls"
"net"
"net/http"
"github.com/go-ldap/ldap/v3"
)
func logRequest(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
handler.ServeHTTP(w, r)
})
}
func ldapOpen(w http.ResponseWriter) (*ldap.Conn, error) {
if config.LdapTLS {
tlsConf := &tls.Config{
ServerName: config.LdapServerAddr,
InsecureSkipVerify: true,
}
return ldap.DialTLS("tcp", net.JoinHostPort(config.LdapServerAddr, "636"), tlsConf)
} else {
return ldap.DialURL("ldap://" + config.LdapServerAddr)
}
// if err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// log.Printf(fmt.Sprintf("27: %v %v", err, l))
// return nil
// }
// return l
}

17
utils-ldap.go Normal file
View file

@ -0,0 +1,17 @@
/*
Utilities related to LDAP
*/
package main
import (
"github.com/go-ldap/ldap/v3"
)
func replaceIfContent(modifReq *ldap.ModifyRequest, key string, value string, previousValue string) error {
if value != "" {
modifReq.Replace(key, []string{value})
} else if previousValue != "" {
modifReq.Delete(key, []string{previousValue})
}
return nil
}

10
utils-ssha.go Normal file
View file

@ -0,0 +1,10 @@
package main
import (
"github.com/jsimonetti/pwscheme/ssha512"
)
// Encode encodes the []byte of raw password
func SSHAEncode(rawPassPhrase string) (string, error) {
return ssha512.Generate(rawPassPhrase, 16)
}

1021
view-admin.go Normal file

File diff suppressed because it is too large Load diff

40
view-home.go Normal file
View file

@ -0,0 +1,40 @@
/*
home show the home page
*/
package main
import "net/http"
func handleHome(w http.ResponseWriter, r *http.Request) {
templateHome := getTemplate("home.html")
login := checkLogin(w, r)
if login == nil {
status := handleLogin(w, r)
if status == nil {
return
}
login = checkLogin(w, r)
}
can_admin := false
if login != nil {
can_admin = login.Common.CanAdmin
}
data := HomePageData{
Login: NestedLoginTplData{
Login: login,
},
BaseDN: config.BaseDN,
Org: config.Org,
Common: NestedCommonTplData{
CanAdmin: can_admin,
CanInvite: true,
LoggedIn: true,
},
}
templateHome.Execute(w, data)
}

555
view-invite.go Normal file
View file

@ -0,0 +1,555 @@
package main
import (
"bytes"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"fmt"
"html/template"
"log"
"net/http"
"regexp"
"strings"
// "github.com/emersion/go-sasl"
// "github.com/emersion/go-smtp"
"github.com/go-ldap/ldap/v3"
"github.com/gorilla/mux"
"golang.org/x/crypto/argon2"
)
var EMAIL_REGEXP = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
func checkInviterLogin(w http.ResponseWriter, r *http.Request) *LoginStatus {
login := checkLogin(w, r)
if login == nil {
return nil
}
// if !login.CanInvite {
// http.Error(w, "Not authorized to invite new users.", http.StatusUnauthorized)
// return nil
// }
return login
}
// New account creation directly from interface
func openNewUserLdap(config *ConfigFile) (*ldap.Conn, error) {
l, err := openLdap(config)
if err != nil {
log.Printf(fmt.Sprintf("openNewUserLdap 1 : %v %v", err, l))
log.Printf(fmt.Sprintf("openNewUserLdap 1 : %v", config))
// data.Common.ErrorMessage = err.Error()
}
err = l.Bind(config.NewUserDN, config.NewUserPassword)
if err != nil {
log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", err))
log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", config.NewUserDN))
log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", config.NewUserPassword))
log.Printf(fmt.Sprintf("openNewUserLdap 2 : %v", config))
// data.Common.ErrorMessage = err.Error()
}
return l, err
}
func handleLostPassword(w http.ResponseWriter, r *http.Request) {
templateLostPasswordPage := getTemplate("passwd/lost.html")
if checkLogin(w, r) != nil {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
data := PasswordLostData{
Common: NestedCommonTplData{
CanAdmin: false,
LoggedIn: false},
}
if r.Method == "POST" {
r.ParseForm()
data.Username = strings.TrimSpace(strings.Join(r.Form["username"], ""))
data.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], ""))
data.OtherMailbox = strings.TrimSpace(strings.Join(r.Form["othermailbox"], ""))
user := User{
CN: strings.TrimSpace(strings.Join(r.Form["username"], "")),
UID: strings.TrimSpace(strings.Join(r.Form["username"], "")),
Mail: strings.TrimSpace(strings.Join(r.Form["mail"], "")),
OtherMailbox: strings.TrimSpace(strings.Join(r.Form["othermailbox"], "")),
}
ldapConn, err := openNewUserLdap(config)
if err != nil {
log.Printf(fmt.Sprintf("handleLostPassword 99 : %v %v", err, ldapConn))
data.Common.ErrorMessage = err.Error()
}
err = passwordLost(user, config, ldapConn)
if err != nil {
log.Printf(fmt.Sprintf("handleLostPassword 104 : %v %v", err, ldapConn))
data.Common.ErrorMessage = err.Error()
} else {
err = ldapConn.Bind(config.NewUserDN, config.NewUserPassword)
if err != nil {
log.Printf(fmt.Sprintf("handleLostPassword 109 : %v %v", err, ldapConn))
data.Common.ErrorMessage = err.Error()
} else {
data.Common.Success = true
}
}
}
data.Common.CanAdmin = false
templateLostPasswordPage.Execute(w, data)
}
func handleInviteNewAccount(w http.ResponseWriter, r *http.Request) {
l, err := ldapOpen(w)
if err != nil {
log.Printf(fmt.Sprintf("58: %v %v", err, l))
}
// l.Bind(config.NewUserDN, config.NewUserPassword)
// login := checkInviterLogin(w, r)
// if login == nil {
// return
// }
// l, _ := ldap.DialURL(config.LdapServerAddr)
// l.Bind(config.NewUserDN, config.NewUserPassword)
// loginInfo, err := doLogin(w, r, "testuser", config.NewUserDN, config.NewUserPassword)
// if err != nil {
// log.Printf(fmt.Sprintf("58: %v %v", err, l))
// }
// l := ldapOpen(w)
if l == nil {
return
}
err = l.Bind(config.NewUserDN, config.NewUserPassword)
if err != nil {
log.Printf(fmt.Sprintf("58: %v %v", err, l))
}
handleNewAccount(w, r, l, config.NewUserDN)
}
// New account creation using code
func handleInvitationCode(w http.ResponseWriter, r *http.Request) {
code := mux.Vars(r)["code"]
code_id, code_pw := readCode(code)
// log.Printf(code_pw)
login := checkLogin(w, r)
// l := ldapOpen(w)
// if l == nil {
// return
// }
inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN
err := login.conn.Bind(inviteDn, code_pw)
if err != nil {
templateInviteInvalidCode := getTemplate("user/code/invalid.html")
templateInviteInvalidCode.Execute(w, nil)
return
}
sReq := ldap.NewSearchRequest(
inviteDn,
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(objectclass=*)"),
[]string{"dn", "creatorsname"},
nil)
sr, err := login.conn.Search(sReq)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(sr.Entries) != 1 {
http.Error(w, fmt.Sprintf("Expected 1 entry, got %d", len(sr.Entries)), http.StatusInternalServerError)
return
}
invitedBy := sr.Entries[0].GetAttributeValue("creatorsname")
if handleNewAccount(w, r, login.conn, invitedBy) {
del_req := ldap.NewDelRequest(inviteDn, nil)
err = login.conn.Del(del_req)
if err != nil {
log.Printf("Could not delete invitation %s: %s", inviteDn, err)
}
}
}
// Common functions for new account
func handleNewAccount(w http.ResponseWriter, r *http.Request, l *ldap.Conn, invitedBy string) bool {
templateInviteNewAccount := getTemplate("user/new.html")
data := &NewAccountData{}
if r.Method == "POST" {
r.ParseForm()
newUser := User{}
// login := checkLogin(w, r)
// newUser.Mail = fmt.Sprintf("%s@%s", strings.TrimSpace(strings.Join(r.Form["username"], "")), "lesgv.com")
newUser.DisplayName = strings.TrimSpace(strings.Join(r.Form["displayname"], ""))
newUser.GivenName = strings.TrimSpace(strings.Join(r.Form["givenname"], ""))
newUser.SN = strings.TrimSpace(strings.Join(r.Form["surname"], ""))
newUser.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], ""))
newUser.UID = strings.TrimSpace(strings.Join(r.Form["otheremail"], ""))
newUser.CN = strings.TrimSpace(strings.Join(r.Form["username"], ""))
newUser.DN = "cn=" + strings.TrimSpace(strings.Join(r.Form["username"], "")) + "," + config.InvitationBaseDN
password1 := strings.Join(r.Form["password"], "")
password2 := strings.Join(r.Form["password2"], "")
if password1 != password2 {
data.Common.Success = false
data.ErrorPasswordMismatch = true
} else {
newUser.Password = password2
l.Bind(config.NewUserDN, config.NewUserPassword)
err := add(newUser, config, l)
if err != nil {
data.Common.Success = false
data.Common.ErrorMessage = err.Error()
}
http.Redirect(w, r, "/admin/activate", http.StatusFound)
}
// tryCreateAccount(l, data, password1, password2, invitedBy)
} else {
data.SuggestPW = fmt.Sprintf("%s", suggestPassword())
}
data.Common.CanAdmin = false
data.Common.LoggedIn = false
templateInviteNewAccount.Execute(w, data)
return data.Common.Success
}
func tryCreateAccount(l *ldap.Conn, data *NewAccountData, pass1 string, pass2 string, invitedBy string) {
checkFailed := false
// Check if username is correct
if match, err := regexp.MatchString("^[a-z0-9._-]+$", data.Username); !(err == nil && match) {
data.ErrorInvalidUsername = true
checkFailed = true
}
// Check if user exists
userDn := config.UserNameAttr + "=" + data.Username + "," + config.UserBaseDN
searchRq := ldap.NewSearchRequest(
userDn,
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
"(objectclass=*)",
[]string{"dn"},
nil)
sr, err := l.Search(searchRq)
if err != nil {
data.Common.ErrorMessage = err.Error()
checkFailed = true
}
if len(sr.Entries) > 0 {
data.ErrorUsernameTaken = true
checkFailed = true
}
// Check that password is long enough
if len(pass1) < 8 {
data.ErrorPasswordTooShort = true
checkFailed = true
}
if pass1 != pass2 {
data.ErrorPasswordMismatch = true
checkFailed = true
}
if checkFailed {
return
}
// Actually create user
req := ldap.NewAddRequest(userDn, nil)
req.Attribute("objectclass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"})
req.Attribute("structuralobjectclass", []string{"inetOrgPerson"})
pw, err := SSHAEncode(pass1)
if err != nil {
data.Common.ErrorMessage = err.Error()
return
}
req.Attribute("userpassword", []string{pw})
req.Attribute("invitedby", []string{invitedBy})
if len(data.DisplayName) > 0 {
req.Attribute("displayname", []string{data.DisplayName})
}
if len(data.GivenName) > 0 {
req.Attribute("givenname", []string{data.GivenName})
}
if len(data.Surname) > 0 {
req.Attribute("sn", []string{data.Surname})
}
if len(config.InvitedMailFormat) > 0 {
email := strings.ReplaceAll(config.InvitedMailFormat, "{}", data.Username)
req.Attribute("mail", []string{email})
}
err = l.Add(req)
if err != nil {
data.Common.ErrorMessage = err.Error()
return
}
for _, group := range config.InvitedAutoGroups {
req := ldap.NewModifyRequest(group, nil)
req.Add("member", []string{userDn})
err = l.Modify(req)
if err != nil {
data.Common.WarningMessage += fmt.Sprintf("Cannot add to %s: %s\n", group, err.Error())
}
}
data.Common.Success = true
}
// ---- Code generation ----
func handleInviteSendCode(w http.ResponseWriter, r *http.Request) {
templateInviteSendCode := getTemplate("user/code/send.html")
login := checkInviterLogin(w, r)
if login == nil {
return
}
// carLicense
if r.Method == "POST" {
r.ParseForm()
data := &SendCodeData{
WebBaseAddress: config.WebAddress,
}
// modify_request := ldap.NewModifyRequest(login.UserEntry.DN, nil)
// // choice := strings.Join(r.Form["choice"], "")
// // sendto := strings.Join(r.Form["sendto"], "")
code, code_id, code_pw := genCode()
log.Printf(fmt.Sprintf("272: %v %v %v", code, code_id, code_pw))
// // Create invitation object in database
// modify_request.Add("carLicense", []string{fmt.Sprintf("%s,%s,%s",code, code_id, code_pw)})
// err := login.conn.Modify(modify_request)
// if err != nil {
// data.Common.ErrorMessage = err.Error()
// // return
// } else {
// data.Common.Success = true
// data.CodeDisplay = code
// }
log.Printf(fmt.Sprintf("279: %v %v %v", code, code_id, code_pw))
addReq := ldap.NewAddRequest("documentIdentifier="+code_id+","+config.InvitationBaseDN, nil)
addReq.Attribute("objectClass", []string{"top", "document", "simpleSecurityObject"})
addReq.Attribute("cn", []string{code})
addReq.Attribute("userPassword", []string{code_pw})
addReq.Attribute("documentIdentifier", []string{code_id})
log.Printf(fmt.Sprintf("285: %v %v %v", code, code_id, code_pw))
log.Printf(fmt.Sprintf("286: %v", addReq))
err := login.conn.Add(addReq)
if err != nil {
data.Common.ErrorMessage = err.Error()
// return
} else {
data.Common.Success = true
data.CodeDisplay = code
}
data.Common.CanAdmin = login.Common.CanAdmin
templateInviteSendCode.Execute(w, data)
// if choice == "display" || choice == "send" {
// log.Printf(fmt.Sprintf("260: %v %v %v %v", login, choice, sendto, data))
// trySendCode(login, choice, sendto, data)
// }
}
}
func trySendCode(login *LoginStatus, choice string, sendto string, data *SendCodeData) {
log.Printf(fmt.Sprintf("269: %v %v %v %v", login, choice, sendto, data))
// Generate code
code, code_id, code_pw := genCode()
log.Printf(fmt.Sprintf("272: %v %v %v", code, code_id, code_pw))
// Create invitation object in database
// len_base_dn := len(strings.Split(config.BaseDN, ","))
// dn_split := strings.Split(super_dn, ",")
// for i := len_base_dn + 1; i <= len(dn_split); i++ {
// path = append(path, PathItem{
// DN: strings.Join(dn_split[len(dn_split)-i:len(dn_split)], ","),
// Identifier: dn_split[len(dn_split)-i],
// })
// }
// data := &SendCodeData{
// WebBaseAddress: config.WebAddress,
// }
// // Handle data
// data := &CreateData{
// SuperDN: super_dn,
// Path: path,
// }
// data.IdType = config.UserNameAttr
// data.StructuralObjectClass = "inetOrgPerson"
// data.ObjectClass = "inetOrgPerson\norganizationalPerson\nperson\ntop"
// data.IdValue = strings.TrimSpace(strings.Join(r.Form["idvalue"], ""))
// data.DisplayName = strings.TrimSpace(strings.Join(r.Form["displayname"], ""))
// data.GivenName = strings.TrimSpace(strings.Join(r.Form["givenname"], ""))
// data.Mail = strings.TrimSpace(strings.Join(r.Form["mail"], ""))
// data.Member = strings.TrimSpace(strings.Join(r.Form["member"], ""))
// data.Description = strings.TrimSpace(strings.Join(r.Form["description"], ""))
// data.SN = strings.TrimSpace(strings.Join(r.Form["sn"], ""))
// object_class := []string{}
// for _, oc := range strings.Split(data.ObjectClass, "\n") {
// x := strings.TrimSpace(oc)
// if x != "" {
// object_class = append(object_class, x)
// }
// }
// dn := data.IdType + "=" + data.IdValue + "," + super_dn
// req := ldap.NewAddRequest(dn, nil)
// req.Attribute("objectclass", object_class)
// // req.Attribute("mail", []string{data.IdValue})
// /*
// if data.StructuralObjectClass != "" {
// req.Attribute("structuralobjectclass", []string{data.StructuralObjectClass})
// }
// */
// if data.DisplayName != "" {
// req.Attribute("displayname", []string{data.DisplayName})
// }
// if data.GivenName != "" {
// req.Attribute("givenname", []string{data.GivenName})
// }
// if data.Mail != "" {
// req.Attribute("mail", []string{data.Mail})
// }
// if data.Member != "" {
// req.Attribute("member", []string{data.Member})
// }
// if data.SN != "" {
// req.Attribute("sn", []string{data.SN})
// }
// if data.Description != "" {
// req.Attribute("description", []string{data.Description})
// }
// err := login.conn.Add(req)
// // log.Printf(fmt.Sprintf("899: %v",err))
// // log.Printf(fmt.Sprintf("899: %v",req))
// // log.Printf(fmt.Sprintf("899: %v",data))
// if err != nil {
// data.Common.Error = err.Error()
// } else {
// if template == "ml" {
// http.Redirect(w, r, "/admin/mailing/"+data.IdValue, http.StatusFound)
// } else {
// http.Redirect(w, r, "/admin/ldap/"+dn, http.StatusFound)
// }
// }
// inviteDn := config.InvitationNameAttr + "=" + code_id + "," + config.InvitationBaseDN
// req := ldap.NewAddRequest(inviteDn, nil)
// pw, err := SSHAEncode(code_pw)
// if err != nil {
// data.Common.ErrorMessage = err.Error()
// return
// }
// req.Attribute("employeeNumber", []string{pw})
// req.Attribute("objectclass", []string{"top", "invitationCode"})
// err = login.conn.Add(req)
// if err != nil {
// log.Printf(fmt.Sprintf("286: %v", req))
// data.Common.ErrorMessage = err.Error()
// return
// }
// If we want to display it, do so
if choice == "display" {
data.Common.Success = true
data.CodeDisplay = code
return
}
// Otherwise, we are sending a mail
if !EMAIL_REGEXP.MatchString(sendto) {
data.ErrorInvalidEmail = true
return
}
templateMail := template.Must(template.ParseFiles(templatePath + "/invite_mail.txt"))
buf := bytes.NewBuffer([]byte{})
templateMail.Execute(buf, &CodeMailFields{
To: sendto,
From: config.MailFrom,
InviteFrom: login.WelcomeName(),
Code: code,
WebBaseAddress: config.WebAddress,
})
log.Printf("Sending mail to: %s", sendto)
// var auth sasl.Client = nil
// if config.SMTPUsername != "" {
// auth = sasl.NewPlainClient("", config.SMTPUsername, config.SMTPPassword)
// }
// err = smtp.SendMail(config.SMTPServer, auth, config.MailFrom, []string{sendto}, buf)
// if err != nil {
// data.Common.ErrorMessage = err.Error()
// return
// }
// log.Printf("Mail sent.")
data.Common.Success = true
data.CodeSentTo = sendto
}
func genCode() (code string, code_id string, code_pw string) {
random := make([]byte, 32)
n, err := rand.Read(random)
if err != nil || n != 32 {
log.Fatalf("Could not generate random bytes: %s", err)
}
a := binary.BigEndian.Uint32(random[0:4])
b := binary.BigEndian.Uint32(random[4:8])
c := binary.BigEndian.Uint32(random[8:12])
code = fmt.Sprintf("%03d-%03d-%03d", a%1000, b%1000, c%1000)
code_id, code_pw = readCode(code)
log.Printf(fmt.Sprintf("342: %v %v %v", code, code_id, code_pw))
return code, code_id, code_pw
}
func readCode(code string) (code_id string, code_pw string) {
// Strip everything that is not a digit
code_digits := ""
for _, c := range code {
if c >= '0' && c <= '9' {
code_digits = code_digits + string(c)
}
}
id_hash := argon2.IDKey([]byte(code_digits), []byte("Guichet ID"), 2, 64*1024, 4, 32)
pw_hash := argon2.IDKey([]byte(code_digits), []byte("Guichet PW"), 2, 64*1024, 4, 32)
code_id = hex.EncodeToString(id_hash[:8])
code_pw = hex.EncodeToString(pw_hash[:16])
return code_id, code_pw
}

128
view-login.go Normal file
View file

@ -0,0 +1,128 @@
/*
login handles login and current-user verification
*/
package main
import (
"fmt"
"log"
"net/http"
"strings"
"github.com/go-ldap/ldap/v3"
)
func (login *LoginStatus) WelcomeName() string {
ret := login.UserEntry.GetAttributeValue("givenName")
if ret == "" {
ret = login.UserEntry.GetAttributeValue("displayName")
}
if ret == "" {
ret = login.Info.Username
}
return ret
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
err := logout(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusFound)
}
func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
templateLogin := getTemplate("login.html")
if r.Method == "POST" {
// log.Printf("%v", "Parsing Form handleLogin")
r.ParseForm()
username := strings.Join(r.Form["username"], "")
password := strings.Join(r.Form["password"], "")
user_dn := fmt.Sprintf("%s=%s,%s", config.UserNameAttr, username, config.UserBaseDN)
// log.Printf("%v", user_dn)
// log.Printf("%v", username)
if strings.EqualFold(username, config.AdminAccount) {
user_dn = username
}
loginInfo, err := doLogin(w, r, username, user_dn, password)
// log.Printf("%v", loginInfo)
if err != nil {
data := &LoginFormData{
Username: username,
Common: NestedCommonTplData{
CanAdmin: false,
CanInvite: true,
LoggedIn: false,
},
}
if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) {
data.WrongPass = true
} else if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
data.WrongUser = true
} else {
log.Printf("%v", err)
log.Printf("%v", user_dn)
log.Printf("%v", username)
data.Common.ErrorMessage = err.Error()
}
templateLogin.Execute(w, data)
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return loginInfo
} else if r.Method == "GET" {
templateLogin.Execute(w, LoginFormData{
Common: NestedCommonTplData{
CanAdmin: false,
CanInvite: true,
LoggedIn: false}})
return nil
} else {
http.Error(w, "Unsupported method", http.StatusBadRequest)
return nil
}
}
func doLogin(w http.ResponseWriter, r *http.Request, username string, user_dn string, password string) (*LoginInfo, error) {
l, _ := ldapOpen(w)
err := l.Bind(user_dn, password)
if err != nil {
log.Printf("doLogin : %v", err)
log.Printf("doLogin : %v", user_dn)
return nil, err
}
// Successfully logged in, save it to session
session, err := store.Get(r, SESSION_NAME)
if err != nil {
session, _ = store.New(r, SESSION_NAME)
}
session.Values["login_username"] = username
session.Values["login_password"] = password
session.Values["login_dn"] = user_dn
err = session.Save(r, w)
if err != nil {
log.Printf("doLogin Session Save: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, err
}
LoginInfo := LoginInfo{
DN: user_dn,
Username: username,
Password: password,
}
return &LoginInfo, nil
}

169
view-passwd.go Normal file
View file

@ -0,0 +1,169 @@
/*
gpas is GVoisin password reset
*/
package main
import (
"bytes"
"errors"
"fmt"
"html/template"
"log"
// "github.com/emersion/go-sasl"
// "github.com/emersion/go-smtp"
"net/smtp"
"github.com/go-ldap/ldap/v3"
// "strings"
b64 "encoding/base64"
)
// type InvitationAccount struct {
// UID string
// Password string
// BaseDN string
// }
// var EMAIL_REGEXP := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
func passwordLost(user User, config *ConfigFile, ldapConn *ldap.Conn) error {
if user.CN == "" && user.Mail == "" && user.OtherMailbox == "" {
return errors.New("Il n'y a pas de quoi identifier l'utilisateur")
}
searchFilter := "(|"
if user.CN != "" {
searchFilter += "(cn=" + user.UID + ")"
}
if user.Mail != "" {
searchFilter += "(mail=" + user.Mail + ")"
}
if user.OtherMailbox != "" {
searchFilter += "(carLicense=" + user.OtherMailbox + ")"
}
searchFilter += ")"
searchReq := ldap.NewSearchRequest(config.UserBaseDN, ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, searchFilter, []string{"cn", "uid", "mail", "carLicense", "sn", "displayName", "givenName"}, nil)
searchRes, err := ldapConn.Search(searchReq)
if err != nil {
log.Printf(fmt.Sprintf("passwordLost 49 : %v %v", err, ldapConn))
log.Printf(fmt.Sprintf("passwordLost 50 : %v", searchReq))
log.Printf(fmt.Sprintf("passwordLost 51: %v", user))
return err
}
if len(searchRes.Entries) == 0 {
log.Printf("Il n'y a pas d'utilisateur qui correspond %v", searchReq)
return errors.New("Il n'y a pas d'utilisateur qui correspond")
}
// log.Printf(fmt.Sprintf("passwordLost 58 : %v", user))
// log.Printf(fmt.Sprintf("passwordLost 59 : %v", searchRes.Entries[0]))
// log.Printf(fmt.Sprintf("passwordLost 60 : %v", searchRes.Entries[0].GetAttributeValue("cn")))
// log.Printf(fmt.Sprintf("passwordLost 61 : %v", searchRes.Entries[0].GetAttributeValue("uid")))
// log.Printf(fmt.Sprintf("passwordLost 62 : %v", searchRes.Entries[0].GetAttributeValue("mail")))
// log.Printf(fmt.Sprintf("passwordLost 63 : %v", searchRes.Entries[0].GetAttributeValue("carLicense")))
// Préparation du courriel à envoyer
user.Password = suggestPassword()
code := b64.URLEncoding.EncodeToString([]byte(user.UID + ";" + user.Password))
user.DN = "uid=" + searchRes.Entries[0].GetAttributeValue("cn") + ",ou=invitations,dc=resdigita,dc=org"
user.UID = searchRes.Entries[0].GetAttributeValue("cn")
user.CN = searchRes.Entries[0].GetAttributeValue("cn")
user.Mail = searchRes.Entries[0].GetAttributeValue("mail")
user.OtherMailbox = searchRes.Entries[0].GetAttributeValue("carLicense")
/* Check for outstanding invitation */
searchReq = ldap.NewSearchRequest(config.InvitationBaseDN, ldap.ScopeBaseObject,
ldap.NeverDerefAliases, 0, 0, false, "(uid="+user.UID+")", []string{"seeAlso"}, nil)
searchRes, err = ldapConn.Search(searchReq)
if err != nil {
log.Printf(fmt.Sprintf("passwordLost (Check existing invitation) : %v", err))
log.Printf(fmt.Sprintf("passwordLost (Check existing invitation) : %v", user))
return err
}
if len(searchRes.Entries) == 0 {
/* Add the invitation */
addReq := ldap.NewAddRequest(
user.DN,
nil)
addReq.Attribute("objectClass", []string{"top", "account", "simpleSecurityObject"})
addReq.Attribute("uid", []string{user.UID})
addReq.Attribute("userPassword", []string{"absdefghi"})
addReq.Attribute("seeAlso", []string{config.UserNameAttr + "=" + user.UID + "," + config.UserBaseDN})
err = ldapConn.Add(addReq)
if err != nil {
log.Printf(fmt.Sprintf("passwordLost 83 : %v", err))
log.Printf(fmt.Sprintf("passwordLost 84 : %v", user))
// log.Printf(fmt.Sprintf("passwordLost 85 : %v", searchRes.Entries[0]))
// For some reason I get here even if the entry exists already
return err
}
}
err = passwd(user, config, ldapConn)
if err != nil {
log.Printf(fmt.Sprintf("passwordLost 90 : %v", err))
log.Printf(fmt.Sprintf("passwordLost 91 : %v", user))
log.Printf(fmt.Sprintf("passwordLost 92 : %v", searchRes.Entries[0]))
return err
}
templateMail := template.Must(template.ParseFiles(templatePath + "/lost_password_email.txt"))
buf := bytes.NewBuffer([]byte{})
templateMail.Execute(buf, &CodeMailFields{
To: user.OtherMailbox,
From: config.MailFrom,
InviteFrom: user.UID,
Code: code,
WebBaseAddress: config.WebAddress,
})
// message := []byte("Hi " + user.OtherMailbox)
log.Printf("Sending mail to: %s", user.OtherMailbox)
// var auth sasl.Client = nil
// if config.SMTPUsername != "" {
// auth = sasl.NewPlainClient("", config.SMTPUsername, config.SMTPPassword)
// }
message := buf.Bytes()
auth := smtp.PlainAuth("", config.SMTPUsername, config.SMTPPassword, config.SMTPServer)
log.Printf("auth: %v", auth)
err = smtp.SendMail(config.SMTPServer+":587", auth, config.SMTPUsername, []string{user.OtherMailbox}, message)
if err != nil {
log.Printf("email send error %v", err)
return err
}
log.Printf("Mail sent.")
return nil
}
func passwordFound(user User, config *ConfigFile, ldapConn *ldap.Conn) (string, error) {
l, err := openLdap(config)
if err != nil {
log.Printf("passwordFound %v", err)
log.Printf("passwordFound Config : %v", config)
return "", err
}
if user.DN == "" && user.UID != "" {
user.DN = "uid=" + user.UID + ",ou=invitations,dc=resdigita,dc=org"
}
err = l.Bind(user.DN, user.Password)
if err != nil {
log.Printf("passwordFound %v", err)
log.Printf("passwordFound %v", user.DN)
log.Printf("passwordFound %v", user.UID)
return "", err
}
searchReq := ldap.NewSearchRequest(user.DN, ldap.ScopeBaseObject,
ldap.NeverDerefAliases, 0, 0, false, "(uid="+user.UID+")", []string{"seeAlso"}, nil)
var searchRes *ldap.SearchResult
searchRes, err = ldapConn.Search(searchReq)
if err != nil {
log.Printf("passwordFound %v", err)
log.Printf("passwordFound %v", searchReq)
log.Printf("passwordFound %v", ldapConn)
log.Printf("passwordFound %v", searchRes)
return "", err
}
if len(searchRes.Entries) == 0 {
log.Printf("passwordFound %v", err)
log.Printf("passwordFound %v", searchReq)
log.Printf("passwordFound %v", ldapConn)
log.Printf("passwordFound %v", searchRes)
return "", err
}
return searchRes.Entries[0].GetAttributeValue("seeAlso"), err
}

219
view-profile.go Normal file
View file

@ -0,0 +1,219 @@
package main
import (
b64 "encoding/base64"
"fmt"
"log"
"net/http"
"strings"
"github.com/gorilla/mux"
)
func handleProfile(w http.ResponseWriter, r *http.Request) {
templateProfile := getTemplate("profile.html")
login := checkLogin(w, r)
if login == nil {
templatePasswd := getTemplate("passwd.html")
templatePasswd.Execute(w, PasswdTplData{
Common: NestedCommonTplData{
CanAdmin: false,
LoggedIn: false},
})
return
}
data := &ProfileTplData{
Login: NestedLoginTplData{
Status: login,
Login: login,
},
Common: NestedCommonTplData{
CanAdmin: login.Common.CanAdmin,
LoggedIn: true,
ErrorMessage: "",
Success: false,
},
}
data.Mail = login.UserEntry.GetAttributeValue("mail")
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" {
//5MB maximum size files
r.ParseMultipartForm(5 << 20)
user := User{
DN: login.Info.DN,
// CN: ,
GivenName: strings.TrimSpace(strings.Join(r.Form["given_name"], "")),
DisplayName: strings.TrimSpace(strings.Join(r.Form["display_name"], "")),
Mail: strings.TrimSpace(strings.Join(r.Form["mail"], "")),
SN: strings.TrimSpace(strings.Join(r.Form["surname"], "")),
//UID: ,
Description: strings.TrimSpace(strings.Join(r.Form["description"], "")),
// Password: ,
}
if user.DisplayName != "" {
err := modify(user, config, login.conn)
if err != nil {
data.Common.ErrorMessage = "handleProfile : " + err.Error()
} else {
data.Common.Success = true
}
}
findUser, err := get(user, config, login.conn)
if err != nil {
data.Common.ErrorMessage = "handleProfile : " + err.Error()
}
data.DisplayName = findUser.DisplayName
data.GivenName = findUser.GivenName
data.Surname = findUser.SN
data.Description = findUser.Description
data.Mail = findUser.Mail
data.Common.LoggedIn = false
/*
visible := strings.TrimSpace(strings.Join(r.Form["visibility"], ""))
if visible != "" {
visible = "on"
} else {
visible = "off"
}
data.Visibility = visible
*/
/*
profilePicture, err := uploadProfilePicture(w, r, login)
if err != nil {
data.Common.ErrorMessage = err.Error()
}
if profilePicture != "" {
data.ProfilePicture = profilePicture
}
*/
//modify_request.Replace(FIELD_NAME_DIRECTORY_VISIBILITY, []string{data.Visibility})
//modify_request.Replace(FIELD_NAME_DIRECTORY_VISIBILITY, []string{"on"})
//if data.ProfilePicture != "" {
// modify_request.Replace(FIELD_NAME_PROFILE_PICTURE, []string{data.ProfilePicture})
// }
// err := login.conn.Modify(modify_request)
// log.Printf(fmt.Sprintf("Profile:079: %v",modify_request))
// log.Printf(fmt.Sprintf("Profile:079: %v",err))
// log.Printf(fmt.Sprintf("Profile:079: %v",data))
// if err != nil {
// data.Common.ErrorMessage = err.Error()
// } else {
// data.Common.Success = true
// }
}
templateProfile.Execute(w, data)
}
func handleFoundPassword(w http.ResponseWriter, r *http.Request) {
templateFoundPasswordPage := getTemplate("passwd.html")
data := PasswdTplData{
Common: NestedCommonTplData{
CanAdmin: false,
LoggedIn: false},
}
code := mux.Vars(r)["code"]
// code = strings.TrimSpace(strings.Join([]string{code}, ""))
newCode, _ := b64.URLEncoding.DecodeString(code)
ldapConn, err := openNewUserLdap(config)
if err != nil {
log.Printf(fmt.Sprint("handleFoundPassword / openNewUserLdap / %v", err))
data.Common.ErrorMessage = err.Error()
}
codeArray := strings.Split(string(newCode), ";")
user := User{
UID: codeArray[0],
Password: codeArray[1],
DN: "uid=" + codeArray[0] + ",ou=invitations,dc=resdigita,dc=org",
}
user.SeeAlso, err = passwordFound(user, config, ldapConn)
if err != nil {
log.Printf("handleFoundPassword / passwordFound %v", err)
log.Printf("handleFoundPassword / passwordFound %v", err)
data.Common.ErrorMessage = err.Error()
}
if r.Method == "POST" {
r.ParseForm()
password := strings.Join(r.Form["password"], "")
password2 := strings.Join(r.Form["password2"], "")
if len(password) < 8 {
data.TooShortError = true
} else if password2 != password {
data.NoMatchError = true
} else {
err := passwd(User{
DN: user.SeeAlso,
Password: password,
}, config, ldapConn)
if err != nil {
data.Common.ErrorMessage = err.Error()
} else {
data.Common.Success = true
}
}
}
data.Common.CanAdmin = false
templateFoundPasswordPage.Execute(w, data)
}
func handlePasswd(w http.ResponseWriter, r *http.Request) {
templatePasswd := getTemplate("passwd.html")
data := &PasswdTplData{
Common: NestedCommonTplData{
CanAdmin: false,
LoggedIn: true,
ErrorMessage: "",
Success: false,
},
}
login := checkLogin(w, r)
if login == nil {
templatePasswd.Execute(w, data)
return
}
data.Login.Status = login
data.Common.CanAdmin = login.Common.CanAdmin
if r.Method == "POST" {
r.ParseForm()
password := strings.Join(r.Form["password"], "")
password2 := strings.Join(r.Form["password2"], "")
if len(password) < 8 {
data.TooShortError = true
} else if password2 != password {
data.NoMatchError = true
} else {
err := passwd(User{
DN: login.Info.DN,
Password: password,
}, config, login.conn)
if err != nil {
data.Common.ErrorMessage = err.Error()
} else {
data.Common.Success = true
}
}
}
data.Common.CanAdmin = false
templatePasswd.Execute(w, data)
}