Anonymous contact authentication for events
This commit is contained in:
parent
a166c72cf3
commit
a3ba16c9b2
5
blog.go
5
blog.go
|
@ -4,6 +4,7 @@ package blog
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
@ -56,6 +57,10 @@ type Blog struct {
|
|||
|
||||
// New initializes the Blog application.
|
||||
func New(documentRoot, userRoot string) *Blog {
|
||||
// Initialize the SQLite database.
|
||||
if _, err := os.Stat(filepath.Join(userRoot, ".private")); os.IsNotExist(err) {
|
||||
os.MkdirAll(filepath.Join(userRoot, ".private"), 0755)
|
||||
}
|
||||
db, err := gorm.Open("sqlite3", filepath.Join(userRoot, ".private", "database.sqlite"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
|
@ -7,7 +7,9 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite" // SQLite DB
|
||||
"github.com/kirsle/blog"
|
||||
|
@ -32,6 +34,7 @@ func init() {
|
|||
flag.BoolVar(&fDebug, "d", false, "Debug mode (alias)")
|
||||
flag.StringVar(&fAddress, "address", ":8000", "Bind address")
|
||||
flag.StringVar(&fAddress, "a", ":8000", "Bind address (alias)")
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
88
internal/controllers/events/contact-auth.go
Normal file
88
internal/controllers/events/contact-auth.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/kirsle/blog/internal/log"
|
||||
"github.com/kirsle/blog/internal/responses"
|
||||
"github.com/kirsle/blog/internal/sessions"
|
||||
"github.com/kirsle/blog/models/contacts"
|
||||
"github.com/kirsle/blog/models/events"
|
||||
)
|
||||
|
||||
// AuthedContact returns the current authenticated Contact, if any, on the session.
|
||||
func AuthedContact(r *http.Request) (contacts.Contact, error) {
|
||||
session := sessions.Get(r)
|
||||
if contactID, ok := session.Values["contact-id"].(int); ok && contactID != 0 {
|
||||
contact, err := contacts.Get(contactID)
|
||||
return contact, err
|
||||
}
|
||||
return contacts.Contact{}, errors.New("not authenticated")
|
||||
}
|
||||
|
||||
// contactAuthHandler listens at "/c/<contact secret>?e=<event id>"
|
||||
//
|
||||
// It is used in RSVP invite emails so when the user clicks the link, it auto
|
||||
// authenticates their session as the contact ID using the contact secret
|
||||
// (a randomly generated string in the DB). The ?e= param indicates an event
|
||||
// ID to redirect to.
|
||||
func contactAuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
secret, ok := params["secret"]
|
||||
if !ok {
|
||||
responses.BadRequest(w, r, "Bad Request")
|
||||
}
|
||||
|
||||
c, err := contacts.GetBySecret(secret)
|
||||
if err != nil {
|
||||
log.Error("contactAuthHandler (/c/<secret>): secret not found, don't know this user")
|
||||
responses.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("contactAuthHandler: Contact %d (%s) is now authenticated", c.ID, c.Name())
|
||||
|
||||
// Authenticate the contact in the session.
|
||||
session := sessions.Get(r)
|
||||
session.Values["contact-id"] = c.ID
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
log.Error("contactAuthHandler: save session error: %s", err)
|
||||
}
|
||||
|
||||
// Did they give us an event ID?
|
||||
eventIDStr := r.FormValue("e")
|
||||
if eventIDStr != "" {
|
||||
if eventID, err := strconv.Atoi(eventIDStr); err == nil {
|
||||
event, err := events.Load(eventID)
|
||||
if err != nil {
|
||||
responses.FlashAndRedirect(w, r, "/", "Event %d not found", eventID)
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to the event.
|
||||
responses.Redirect(w, "/e/"+event.Fragment)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect home I guess?
|
||||
log.Error("contactAuthHandler: don't know where to send them (no ?e= param for event ID)")
|
||||
responses.Redirect(w, "/")
|
||||
}
|
||||
|
||||
func contactLogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
session := sessions.Get(r)
|
||||
delete(session.Values, "contact-id")
|
||||
session.Save(r, w)
|
||||
|
||||
if next := r.FormValue("next"); next != "" && strings.HasPrefix(next, "/") {
|
||||
responses.Redirect(w, next)
|
||||
} else {
|
||||
responses.Redirect(w, "/")
|
||||
}
|
||||
}
|
|
@ -29,6 +29,8 @@ func Register(r *mux.Router, loginError http.HandlerFunc) {
|
|||
|
||||
// Public routes
|
||||
r.HandleFunc("/e/{fragment}", viewHandler)
|
||||
r.HandleFunc("/c/logout", contactLogoutHandler)
|
||||
r.HandleFunc("/c/{secret}", contactAuthHandler)
|
||||
}
|
||||
|
||||
// Admin index to view all events.
|
||||
|
@ -60,10 +62,50 @@ func viewHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Template variables.
|
||||
v := map[string]interface{}{
|
||||
"event": event,
|
||||
"authedRSVP": events.RSVP{},
|
||||
}
|
||||
|
||||
// Sort the guest list.
|
||||
sort.Sort(events.ByName(event.RSVP))
|
||||
|
||||
v := map[string]interface{}{
|
||||
"event": event,
|
||||
// Is the browser session authenticated as a contact?
|
||||
authedContact, err := AuthedContact(r)
|
||||
if err == nil {
|
||||
v["authedContact"] = authedContact
|
||||
|
||||
// Do they have an RSVP?
|
||||
for _, rsvp := range event.RSVP {
|
||||
if rsvp.ContactID == authedContact.ID {
|
||||
v["authedRSVP"] = rsvp
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're posting, are we RSVPing?
|
||||
if r.Method == http.MethodPost {
|
||||
action := r.PostFormValue("action")
|
||||
switch action {
|
||||
case "answer-rsvp":
|
||||
answer := r.PostFormValue("submit")
|
||||
for _, rsvp := range event.RSVP {
|
||||
if rsvp.ContactID == authedContact.ID {
|
||||
log.Info("Mark RSVP status %s for contact %s", answer, authedContact.Name())
|
||||
rsvp.Status = answer
|
||||
rsvp.Save()
|
||||
responses.FlashAndReload(w, r, "You have confirmed '%s' for your RSVP.", answer)
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
responses.FlashAndReload(w, r, "Invalid form action.")
|
||||
}
|
||||
responses.FlashAndReload(w, r, "Unknown error.")
|
||||
return
|
||||
}
|
||||
|
||||
render.Template(w, r, "events/view", v)
|
||||
}
|
||||
|
|
|
@ -16,15 +16,26 @@ func notifyUser(ev *events.Event, rsvp events.RSVP) {
|
|||
)
|
||||
s, _ := settings.Load()
|
||||
|
||||
// Can we get an "auto-login" link?
|
||||
var claimURL string
|
||||
if rsvp.Contact.Secret != "" {
|
||||
claimURL = fmt.Sprintf("%s/c/%s?e=%d",
|
||||
strings.Trim(s.Site.URL, "/"),
|
||||
rsvp.Contact.Secret,
|
||||
ev.ID,
|
||||
)
|
||||
}
|
||||
|
||||
// Do they have... an e-mail address?
|
||||
if email != "" {
|
||||
mail.SendEmail(mail.Email{
|
||||
go mail.SendEmail(mail.Email{
|
||||
To: email,
|
||||
Subject: fmt.Sprintf("Invitation to: %s", ev.Title),
|
||||
Data: map[string]interface{}{
|
||||
"RSVP": rsvp,
|
||||
"Event": ev,
|
||||
"URL": strings.Trim(s.Site.URL, "/") + "/e/" + ev.Fragment,
|
||||
"RSVP": rsvp,
|
||||
"Event": ev,
|
||||
"URL": strings.Trim(s.Site.URL, "/") + "/e/" + ev.Fragment,
|
||||
"ClaimURL": claimURL,
|
||||
},
|
||||
Template: ".email/event-invite.gohtml",
|
||||
})
|
||||
|
@ -32,7 +43,7 @@ func notifyUser(ev *events.Event, rsvp events.RSVP) {
|
|||
|
||||
// An SMS number?
|
||||
if sms != "" {
|
||||
|
||||
// TODO: Twilio
|
||||
}
|
||||
|
||||
rsvp.Notified = true
|
||||
|
|
|
@ -2,6 +2,7 @@ package contacts
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -65,6 +66,7 @@ func (c *Contact) presave() {
|
|||
secret[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
c.Secret = string(secret)
|
||||
fmt.Printf("contact.presave: secret=%s", string(secret))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,6 +113,13 @@ func GetSMS(number string) (Contact, error) {
|
|||
return contact, err
|
||||
}
|
||||
|
||||
// GetBySecret queries a contact by their secret.
|
||||
func GetBySecret(secret string) (Contact, error) {
|
||||
contact := Contact{}
|
||||
err := DB.Where("secret = ?", secret).First(&contact).Error
|
||||
return contact, err
|
||||
}
|
||||
|
||||
// Name returns a friendly name for the contact.
|
||||
func (c Contact) Name() string {
|
||||
var parts []string
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
To view the details and RSVP, visit the link below:
|
||||
<br><br>
|
||||
|
||||
<a href="{{ .Data.URL }}" target="_blank">{{ .Data.URL }}</a>
|
||||
<a href="{{ or .Data.ClaimURL .Data.URL }}" target="_blank">{{ or .Data.ClaimURL .Data.URL }}</a>
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
{{ else }}
|
||||
<strong>{{ $rsvp.Name }}</strong>
|
||||
{{ end }}
|
||||
<button type="submit" class="btn btn-sm btn-danger">delete</button>
|
||||
<button type="submit" class="btn btn-sm btn-danger">uninvite</button>
|
||||
</form>
|
||||
<ul class="list-inline">
|
||||
{{ if .Contact }}
|
||||
|
|
|
@ -1,7 +1,32 @@
|
|||
{{ define "title" }}{{ .Data.event.Title }}{{ end }}
|
||||
{{ define "content" }}
|
||||
|
||||
{{ $authedContact := .Data.authedContact }}
|
||||
{{ $authedRSVP := .Data.authedRSVP }}
|
||||
|
||||
{{ with .Data.event }}
|
||||
{{ if and $authedContact $authedRSVP.ID }}
|
||||
<div class="row mb-4">
|
||||
<div class="col-8">
|
||||
<p>
|
||||
<strong>{{ $authedContact.Name }}</strong>, you have been invited to...
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-4 text-right">
|
||||
<form name="rsvpAnswerForm" action="/e/{{ .Fragment }}" method="POST">
|
||||
<input type="hidden" name="_csrf" value="{{ $.CSRF }}">
|
||||
<input type="hidden" name="action" value="answer-rsvp">
|
||||
<div class="btn-group">
|
||||
<button type="submit" name="submit" value="going" class="btn{{ if eq $authedRSVP.Status "going" }} btn-success{{ end }}">Going</button>
|
||||
<button type="submit" name="submit" value="maybe" class="btn{{ if eq $authedRSVP.Status "maybe" }} btn-warning{{ end }}">Maybe</button>
|
||||
<button type="submit" name="submit" value="not going" class="btn{{ if eq $authedRSVP.Status "not going" }} btn-danger{{ end }}">Not Going</button>
|
||||
</div>
|
||||
<p class="small">
|
||||
[<a href="/c/logout?next={{ $.Request.URL.Path }}">not {{ $authedContact.Name }}?</a>]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<h1>{{ .Title }}</h1>
|
||||
|
||||
<div class="row mb-4">
|
||||
|
@ -36,9 +61,9 @@
|
|||
{{ range .RSVP }}
|
||||
<li class="list-group-item
|
||||
{{ if eq .Status "invited" }}bg-light
|
||||
{{ else if eq .Status "going"}}bg-success text-light
|
||||
{{ else if eq .Status "not going"}}bg-danger text-light
|
||||
{{ else if eq .Status "maybe"}}bg-warning text-light{{ end }}">
|
||||
{{ else if eq .Status "going"}}border-success
|
||||
{{ else if eq .Status "not going"}}border-danger
|
||||
{{ else if eq .Status "maybe"}}border-warning{{ end }}">
|
||||
{{ if .Contact }}
|
||||
<strong>{{ .Contact.Name }}</strong>
|
||||
{{ else }}
|
||||
|
@ -51,7 +76,7 @@
|
|||
{{ if not .Notified }}
|
||||
<span class="badge badge-warning">not notified</span>
|
||||
{{ end }}
|
||||
<ul class="list-inline">
|
||||
<ul class="list-inline small">
|
||||
{{ if .Contact }}
|
||||
{{ if .Contact.Email }}
|
||||
<li class="list-inline-item text-muted">
|
||||
|
|
Loading…
Reference in New Issue
Block a user