Add amiga invites

merge-requests/4/head
meskio 4 years ago
parent 19c93d6648
commit 971482da17
Signed by: meskio
GPG Key ID: 52B8F5AC97A2DA86
  1. 11
      db/db.go
  2. 64
      db/db_test.go
  3. 55
      db/invite.go
  4. 56
      db/invite_test.go
  5. 19
      main.go
  6. 104
      server/add_user.go
  7. 49
      server/admin.go
  8. 7
      server/server.go
  9. 2
      server/template.go
  10. 2
      server/user.go
  11. 34
      tmpl/adduser.html
  12. 16
      tmpl/adduser_success.html
  13. 3
      tmpl/index.html
  14. 18
      tmpl/invite.html
  15. 6
      tmpl/navbar.html
  16. 2
      tmpl/password.html

@ -20,6 +20,10 @@ func Init(path string) (*DB, error) {
err = bolt.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(askSindominanteBucket)
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists(inviteBucket)
return err
})
if err != nil {
@ -61,6 +65,13 @@ func (db *DB) get(bucket []byte, key string, value interface{}) error {
return json.Unmarshal(encodedValue, value)
}
func (db *DB) del(bucket []byte, key string) error {
return db.bolt.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket(bucket)
return b.Delete([]byte(key))
})
}
type notFoundError struct{}
func (e notFoundError) Error() string {

@ -0,0 +1,64 @@
package db
import (
"io/ioutil"
"os"
"path"
"testing"
)
const (
key = "key"
value = "value"
)
func initTestDB(t *testing.T) *DB {
dir, err := ioutil.TempDir("", "lowry_test")
if err != nil {
t.Fatalf("Can't create a temp dir: %v", err)
}
db, err := Init(path.Join(dir, "bolt.db"))
if err != nil {
t.Fatalf("Can't init db (%s): %v", dir, err)
}
return db
}
func delTestDB(db *DB) {
dir := path.Dir(db.bolt.Path())
db.Close()
os.RemoveAll(dir)
}
func TestPutGet(t *testing.T) {
db := initTestDB(t)
defer delTestDB(db)
err := db.get(inviteBucket, key, value)
if _, ok := err.(notFoundError); !ok {
t.Errorf("Got something else than notFoundError before put: %v", err)
}
err = db.put(inviteBucket, key, value)
if err != nil {
t.Fatalf("Got an error putting: %v", err)
}
var v string
err = db.get(inviteBucket, key, &v)
if err != nil {
t.Fatalf("Got an error getting: %v", err)
}
if v != value {
t.Fatalf("Expected %v got %v", value, v)
}
err = db.del(inviteBucket, key)
if err != nil {
t.Fatalf("Got an error deleting: %v", err)
}
err = db.get(inviteBucket, key, value)
if _, ok := err.(notFoundError); !ok {
t.Errorf("Got something else than notFoundError after delete: %v", err)
}
}

@ -0,0 +1,55 @@
package db
import (
"encoding/json"
"log"
"time"
"go.etcd.io/bbolt"
)
var (
inviteBucket = []byte("invite")
)
type invite struct {
User string
CreationDate time.Time
}
// AddInvite stores in the db the invite code generated by user
func (db *DB) AddInvite(code string, user string) error {
return db.put(inviteBucket, code, invite{user, time.Now()})
}
// IsInviteValid checks if the invite code is in the database
func (db *DB) IsInviteValid(code string) bool {
var inv invite
err := db.get(inviteBucket, code, &inv)
return err == nil
}
// DelInvite code from the database
func (db *DB) DelInvite(code string) error {
return db.del(inviteBucket, code)
}
// ExpireInvites older than duration
func (db *DB) ExpireInvites(duration time.Duration) error {
return db.bolt.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket(inviteBucket)
return b.ForEach(func(k, v []byte) error {
var inv invite
err := json.Unmarshal(v, &inv)
if err != nil {
log.Printf("Error unmarshalling invite %s: %v", string(k), err)
return nil
}
if inv.CreationDate.Add(duration).Before(time.Now()) {
return b.Delete(k)
}
return nil
})
})
}

@ -0,0 +1,56 @@
package db
import (
"testing"
"time"
)
const (
code = "code"
)
func TestAddInvite(t *testing.T) {
db := initTestDB(t)
defer delTestDB(db)
if db.IsInviteValid(code) {
t.Errorf("Got valid invite before adding it")
}
err := db.AddInvite(code, "")
if err != nil {
t.Fatalf("Got an error adding invite: %v", err)
}
if !db.IsInviteValid(code) {
t.Errorf("Got invalid invite after adding it")
}
err = db.DelInvite(code)
if err != nil {
t.Fatalf("Got an error deleting invite: %v", err)
}
if db.IsInviteValid(code) {
t.Errorf("Got valid invite deleting it")
}
}
func TestExpireInvites(t *testing.T) {
db := initTestDB(t)
defer delTestDB(db)
err := db.AddInvite(code, "")
if err != nil {
t.Fatalf("Got an error adding invite: %v", err)
}
if !db.IsInviteValid(code) {
t.Errorf("Got invalid invite after adding it")
}
err = db.ExpireInvites(time.Microsecond)
if err != nil {
t.Fatalf("Got an error expiring invites: %v", err)
}
if db.IsInviteValid(code) {
t.Errorf("Got valid invite after expiring it")
}
}

@ -6,6 +6,7 @@ import (
"os"
"sort"
"strings"
"time"
"0xacab.org/sindominio/lowry/db"
"0xacab.org/sindominio/lowry/ldap"
@ -13,6 +14,10 @@ import (
"github.com/namsral/flag"
)
var (
inviteExpireDuration = time.Hour * 24 * 30
)
func main() {
var (
ldapaddr = flag.String("ldapaddr", "localhost:389", "LDAP server address and port")
@ -39,18 +44,26 @@ func main() {
log.Fatal(err)
}
db, err := db.Init(*dbpath)
ldb, err := db.Init(*dbpath)
if err != nil {
log.Fatal(err)
}
defer db.Close()
defer ldb.Close()
go cleanInvites(ldb)
usersAskRole := []string{}
if *askRolePath != "" {
usersAskRole = readUserList(*askRolePath)
}
log.Fatal(server.Serve(*httpaddr, &l, db, usersAskRole))
log.Fatal(server.Serve(*httpaddr, &l, ldb, usersAskRole))
}
func cleanInvites(ldb *db.DB) {
for {
ldb.ExpireInvites(inviteExpireDuration)
time.Sleep(time.Minute * 60)
}
}
func readUserList(listPath string) []string {

@ -0,0 +1,104 @@
package server
import (
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"net/http"
"0xacab.org/sindominio/lowry/ldap"
"github.com/gorilla/mux"
)
func (s *server) createInviteHandler(w http.ResponseWriter, r *http.Request) {
response := s.newResponse("invite", w, r)
if response.Role != ldap.Sindominante {
log.Printf("Non sindominante attemp to create an invite, user: %s", response.User)
s.forbiddenHandler(w, r)
return
}
buff := make([]byte, 9)
_, err := rand.Read(buff)
if err != nil {
log.Printf("An error has ocurred generating a random invite: %v", err)
s.addUserGroupHandler(w, r)
return
}
invite := base64.URLEncoding.EncodeToString(buff)
err = s.db.AddInvite(invite, response.User)
if err != nil {
log.Printf("An error has ocurred storing the invite (%s - %s): %v", invite, response.User, err)
s.addUserGroupHandler(w, r)
return
}
inviteURL := fmt.Sprintf("https://%v/adduser/%v", r.Host, invite)
response.execute(struct {
InviteURL string
}{inviteURL})
}
func (s *server) addUserHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
invite := vars["invite"]
if !s.db.IsInviteValid(invite) {
log.Printf("Invalid invite code: %s", invite)
s.forbiddenHandler(w, r)
return
}
response := s.newResponse("adduser", w, r)
if r.Method != "POST" {
response.execute("")
return
}
name := r.FormValue("name")
pass := r.FormValue("password")
pass2 := r.FormValue("password2")
if pass != pass2 {
response.execute("WrongPass")
return
}
if name == "" || pass == "" {
response.execute("empty")
return
}
_, err := s.ldap.GetUser(name)
if err == nil {
log.Println("Can't create user ", name, ": already exist")
response.execute("exsist")
return
}
err = s.ldap.AddGroup(name)
if err != nil {
log.Println("Error adding group: ", err)
s.errorHandler(w, r)
return
}
group, err := s.ldap.GetGroup(name)
if err != nil {
log.Println("Error getting group: ", err)
s.errorHandler(w, r)
return
}
err = s.ldap.AddUser(name, pass, group.GID)
if err != nil {
log.Println("Error adding user: ", err)
s.errorHandler(w, r)
return
}
err = s.db.DelInvite(invite)
if err != nil {
log.Println("Error deleting invite: ", err)
}
response = s.newResponse("adduser_success", w, r)
response.execute("name")
}

@ -57,55 +57,6 @@ func (s *server) userHandler(w http.ResponseWriter, r *http.Request) {
response.execute(data)
}
func (s *server) addUserHandler(w http.ResponseWriter, r *http.Request) {
response := s.newResponse("adduser", w, r)
if !response.IsAdmin {
log.Println("Non admin attemp to add user")
s.forbiddenHandler(w, r)
return
}
if r.Method != "POST" {
response.execute("")
return
}
name := r.FormValue("name")
pass := r.FormValue("password")
if name == "" || pass == "" {
response.execute("empty")
return
}
_, err := s.ldap.GetUser(name)
if err == nil {
log.Println("Can't create user ", name, ": already exist")
response.execute("exsist")
return
}
err = s.ldap.AddGroup(name)
if err != nil {
log.Println("Error adding group: ", err)
s.errorHandler(w, r)
return
}
group, err := s.ldap.GetGroup(name)
if err != nil {
log.Println("Error getting group: ", err)
s.errorHandler(w, r)
return
}
err = s.ldap.AddUser(name, pass, group.GID)
if err != nil {
log.Println("Error adding user: ", err)
s.errorHandler(w, r)
return
}
http.Redirect(w, r, "/users/"+name, http.StatusFound)
}
func (s *server) groupsHandler(w http.ResponseWriter, r *http.Request) {
response := s.newResponse("groups", w, r)
if !response.IsAdmin {

@ -18,10 +18,10 @@ type server struct {
}
// Serve lowry web site
func Serve(addr string, l *ldap.Ldap, db *db.DB, usersAskRole []string) error {
func Serve(addr string, l *ldap.Ldap, ldb *db.DB, usersAskRole []string) error {
var s server
s.ldap = l
s.db = db
s.db = ldb
s.sess = initSessionStore()
s.tmpl = initTemplate()
s.usersAskRole = usersAskRole
@ -39,7 +39,8 @@ func Serve(addr string, l *ldap.Ldap, db *db.DB, usersAskRole []string) error {
r.HandleFunc("/users/{name}", s.userHandler)
r.HandleFunc("/users/{name}/password/", s.passwdadmHandler).Methods("POST")
r.HandleFunc("/users/{name}/shell/", s.shellHandler).Methods("POST")
r.HandleFunc("/adduser/", s.addUserHandler)
r.HandleFunc("/adduser/", s.createInviteHandler)
r.HandleFunc("/adduser/{invite}", s.addUserHandler)
r.HandleFunc("/groups/", s.groupsHandler)
r.HandleFunc("/groups/{name}", s.groupHandler)
r.HandleFunc("/groups/{name}/add/", s.addUserGroupHandler).Methods("POST")

@ -32,7 +32,9 @@ func initTemplate() *template.Template {
"tmpl/password.html",
"tmpl/user.html",
"tmpl/users.html",
"tmpl/invite.html",
"tmpl/adduser.html",
"tmpl/adduser_success.html",
"tmpl/group.html",
"tmpl/groups.html",
))

@ -61,7 +61,7 @@ func (s *server) passwordHandler(w http.ResponseWriter, r *http.Request) {
pass := r.FormValue("password")
pass2 := r.FormValue("password2")
if pass != pass2 {
response.execute("")
response.execute("WrongPass")
return
}

@ -1,14 +1,21 @@
{{template "header.html"}}
{{template "navbar.html" .}}
<div class="container">
<br />
<h1 class="row justify-content-center">Crear usuaria</h1>
<h1 class="row justify-content-center">Crear cuenta</h1>
<br />
<div class="row justify-content-center">
<div class="col-md-8">
<p>Estas apunto de entrar a formar parte de las amigas de SinDominio. Rellenando este formulario puedes crear una cuenta en sindominio que te permitirá acceder a muchos servicios (como email, jabber o matrix).</p>
<p>SinDominio no es como otros servidores. Esta mantenido por tus vecinas sindominantes cuya preocupación es proteger tus datos en vez de hacer negocio con ellos.</p>
<p>Date una vuelta por <a href="https://wp.sindominio.net/sd_info/">nuestra pagina sobre el proyecto</a> para aprender como funcionamos y como ayudar.</p>
</div>
</div>
<br />
<div class="row justify-content-center">
<form class="col-4" action="/adduser/" method="post">
<form class="col-sm-4" id="needs-validation" method="post">
<div class="form-group">
<label for="name">Nombre de usuaria:</label>
<input type="text" class="form-control {{if eq .Data "exsist" "empty"}}is-invalid{{end}}" id="name" name="name" placeholder="Nombre">
@ -20,9 +27,30 @@
<input type="password" class="form-control {{if eq .Data "empty"}}is-invalid{{end}}" id="password" name="password" placeholder="Contraseña">
{{if eq .Data "empty"}}<div class="invalid-feedback">Ni el nombre de usuaria ni la password pueden estar vacias.</div>{{end}}
</div>
<div class="form-group">
<label for="password2">Repite la contraseña:</label>
<input type="password" class="form-control {{if eq .Data "WrongPass"}}is-invalid{{end}}" id="password2" name="password2" placeholder="Contraseña">
<div class="invalid-feedback">No ha introducido la misma contraseña.</div>
</div>
<button type="submit" class="btn btn-primary">Crear</button>
</form>
</div>
</div>
<script>
(function() {
"use strict";
window.addEventListener("load", function() {
var form = document.getElementById("needs-validation");
form.addEventListener("submit", function(event) {
if (form.password.value !== form.password2.value) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add("was-validated");
}, false);
}, false);
}());
</script>
{{template "footer.html"}}

@ -0,0 +1,16 @@
{{template "header.html"}}
<div class="container">
<br />
<h1 class="row justify-content-center">Usuaria creada</h1>
<br />
<div class="row justify-content-center">
<div class="col-md-8">
<p>Bienvenida a SinDominio. Puedes entrar a <a href="https://lowry.sindominio.net/">gestionar tu cuenta</a>, o ir directamente a la <a href="https://sindominio.net/">portada a recorrer nuestros servicios</a>.<p>
</div>
</div>
</div>
{{template "footer.html"}}

@ -62,6 +62,9 @@
<p>Bienvenida a lowry, nuestro burocra preferido. ¿que quieres hacer hoy?</p>
<ul class="col-md-4 list-group">
<li class="list-group-item"><a href="/password/">Cambiar la contraseña</a></li>
{{if eq (printf "%v" .Role) "sindominante"}}
<li class="list-group-item"><a href="/adduser/">Invitar amiga a sindominio</a></li>
{{end}}
</ul>
</div>
</div>

@ -0,0 +1,18 @@
{{template "header.html"}}
{{template "navbar.html" .}}
<div class="container">
<br />
<h1 class="row justify-content-center">Invitacion a SinDominio</h1>
<br />
<div class="row justify-content-center">
<div class="col-md-8">
<p>Enviale a tu amiga la siguiente dirección para que se pueda crear su cuenta en sindominio: <br />
{{.Data.InviteURL}}<b>
</div>
</div>
</div>
{{template "footer.html"}}

@ -14,17 +14,19 @@
<a class="nav-link" href="/password/">Contraseña</a>
</li>
</ul>
{{if .IsAdmin}}
{{if eq (printf "%v" .Role) "sindominante"}}
<ul class="navbar-nav ml-mr-auto">
<li class="nav-item {{if eq .Section "adduser"}}active{{end}}">
<a class="nav-link" href="/adduser/">Crear usuaria</a>
<a class="nav-link" href="/adduser/">Invitar amiga</a>
</li>
{{if .IsAdmin}}
<li class="nav-item {{if eq .Section "user" "users"}}active{{end}}">
<a class="nav-link" href="/users/">Usuarias</a>
</li>
<li class="nav-item {{if eq .Section "group" "groups"}}active{{end}}">
<a class="nav-link" href="/groups/">Grupos</a>
</li>
{{end}}
</ul>
{{end}}

@ -22,7 +22,7 @@
<input type="password" class="form-control" id="password" name="password" placeholder="Contraseña">
</div>
<div class="form-group">
<label for="password2">Repita la contraseña nueva:</label>
<label for="password2">Repite la contraseña nueva:</label>
<input type="password" class="form-control {{if eq .Data "WrongPass"}}is-invalid{{end}}" id="password2" name="password2" placeholder="Contraseña">
<div class="invalid-feedback">No ha introducido la misma contraseña.</div>
</div>

Loading…
Cancel
Save