~m15o/riku

398aa5727724d9f5b6a3c701dd42ab89de8ea527 — m15o 2 years ago 921eb93
refactoring
28 files changed, 492 insertions(+), 340 deletions(-)

A .gitignore
M assets/static_stylesheet.go
M assets/style.css
M bin/riku
M model/request.go
M model/user.go
M storage/request.go
M storage/sql.go
M storage/sql/schema_version_1.sql
M storage/user.go
M web/handler/common.go
A web/handler/feed_show.go
M web/handler/form/user.go
M web/handler/handler.go
M web/handler/html.go
M web/handler/html/common/layout.html
M web/handler/html/index.html
M web/handler/html/manual.html
M web/handler/html/register.html
D web/handler/html/requests.html
R web/handler/html/{form.html => response.html}
A web/handler/html/responses.html
M web/handler/register.go
M web/handler/request_submit.go
R web/handler/{request_list.go => response_list.go}
R web/handler/{request_close.go => response_move_archive.go}
R web/handler/{request_open.go => response_move_inbox.go}
R web/handler/{form_show.go => response_show.go}
A .gitignore => .gitignore +1 -0
@@ 0,0 1,1 @@
.idea
\ No newline at end of file

M assets/static_stylesheet.go => assets/static_stylesheet.go +21 -2
@@ 155,8 155,11 @@ tr, td, th {
thead {
	background-color: palegreen;
}
tbody {
	background-color: white;
/*tbody {*/
/*	background-color: white;*/
/*}*/
.responses tr:nth-child(2n+1) {
	background-color: palegoldenrod;
}




@@ 247,5 250,21 @@ dd {

.actions {
	display: flex;
}

.meta {
	background-color: palegoldenrod;
}
.meta td, .meta tr {
	border: none;
}

.meta td:first-child {
	font-weight: bold;
}

.response {
	display: flex;
	justify-content: space-between;
}`,
}

M assets/style.css => assets/style.css +21 -2
@@ 150,8 150,11 @@ tr, td, th {
thead {
	background-color: palegreen;
}
tbody {
	background-color: white;
/*tbody {*/
/*	background-color: white;*/
/*}*/
.responses tr:nth-child(2n+1) {
	background-color: palegoldenrod;
}




@@ 242,4 245,20 @@ dd {

.actions {
	display: flex;
}

.meta {
	background-color: palegoldenrod;
}
.meta td, .meta tr {
	border: none;
}

.meta td:first-child {
	font-weight: bold;
}

.response {
	display: flex;
	justify-content: space-between;
}
\ No newline at end of file

M bin/riku => bin/riku +0 -0
M model/request.go => model/request.go +1 -0
@@ 12,6 12,7 @@ type Request struct {
	Closed    bool
	Label     string
	Data      string
	Referer   string
}

func (r Request) Form() map[string][]string {

M model/user.go => model/user.go +0 -2
@@ 5,7 5,6 @@ import "golang.org/x/crypto/bcrypt"
type User struct {
	Id       int64
	Name     string
	Email    string
	Password string
	Hash     string
}


@@ 14,7 13,6 @@ type User struct {
type UserCreationRequest struct {
	Name     string
	Password string
	Email    string
}

func (u User) CompareHashToPassword(hash string) error {

M storage/request.go => storage/request.go +46 -7
@@ 5,15 5,16 @@ import (
	"riku/model"
)

func (s *Storage) Save(userId int64, label, data string) error {
func (s *Storage) Save(userId int64, referer, label, data string) error {
	query := `
        INSERT INTO requests (
            user_id,
            referer,
            data,
            label
        ) VALUES ($1, $2, $3)
        ) VALUES ($1, $2, $3, $4)
    `
	_, err := s.db.Exec(query, userId, data, label)
	_, err := s.db.Exec(query, userId, referer, data, label)
	return err
}



@@ 25,11 26,12 @@ func populateRequest(rows *sql.Rows) (model.Request, error) {
		&rv.Closed,
		&rv.Label,
		&rv.Data,
		&rv.Referer,
	)
	return rv, err
}

func (s *Storage) Requests(userId int64, label string, closed bool) ([]model.Request, error) {
func (s *Storage) Responses(userId int64, label string, closed bool) ([]model.Request, error) {
	if label == "" {
		label = "%"
	}


@@ 39,12 41,14 @@ func (s *Storage) Requests(userId int64, label string, closed bool) ([]model.Req
            created_at,
            closed,
		    label,
            data
            data,
            referer
        FROM requests
        WHERE
            user_id=$1
            AND label LIKE $2
            AND closed=$3
        ORDER BY created_at DESC
    `, userId, label, closed)

	if err != nil {


@@ 66,7 70,7 @@ func (s *Storage) Requests(userId int64, label string, closed bool) ([]model.Req
	return requests, nil
}

func (s *Storage) RequestById(userId, requestId int64) (model.Request, error) {
func (s *Storage) ResponseById(userId, requestId int64) (model.Request, error) {
	var rv model.Request

	rows, err := s.db.Query(`


@@ 75,7 79,8 @@ func (s *Storage) RequestById(userId, requestId int64) (model.Request, error) {
            created_at,
            closed,
		    label,
            data
            data,
		   referer
        FROM requests
        WHERE
            user_id=$1


@@ 97,6 102,40 @@ func (s *Storage) RequestById(userId, requestId int64) (model.Request, error) {
	return rv, nil
}

func (s *Storage) AllResponses(userId int64) ([]model.Request, error) {
	rows, err := s.db.Query(`
        SELECT
            id,
            created_at,
            closed,
		    label,
            data,
            referer
        FROM requests
        WHERE
            user_id=$1
        ORDER BY created_at DESC
    `, userId)

	if err != nil {
		return nil, err
	}

	var requests []model.Request

	for rows.Next() {
		r, err := populateRequest(rows)

		if err != nil {
			return requests, err
		}

		requests = append(requests, r)
	}

	return requests, nil
}

func (s *Storage) Close(requestId, userId int64) error {
	query := `
        UPDATE requests

M storage/sql.go => storage/sql.go +17 -5
@@ 10,17 10,29 @@ var SqlMap = map[string]string{
create table users (
    id int primary key generated always as identity,
    name varchar(15) unique CHECK (name <> ''),
    hash varchar(100) not null CHECK (hash <> ''),
    email varchar(300) not null check (email <> '')
    hash varchar(100) not null CHECK (hash <> '')
);

create table requests (
    id integer primary key generated always as identity,
    id integer not null,
    created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
    closed bool not null default false,
    user_id integer not null references users(id),
    label varchar(50) not null default '',
    data text not null default ''
    data text not null default '',
    referer text not null default '',
    unique (user_id, id)
);
`,

CREATE OR REPLACE FUNCTION assign_request_id() RETURNS TRIGGER AS $$
BEGIN
    SELECT coalesce(max(id), 0) + 1 INTO NEW.id
    FROM requests
    WHERE user_id = NEW.user_id;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tr_assign_request_id
    BEFORE INSERT ON requests
    FOR EACH ROW EXECUTE PROCEDURE assign_request_id();`,
}

M storage/sql/schema_version_1.sql => storage/sql/schema_version_1.sql +17 -4
@@ 5,15 5,28 @@ create table schema_version (
create table users (
    id int primary key generated always as identity,
    name varchar(15) unique CHECK (name <> ''),
    hash varchar(100) not null CHECK (hash <> ''),
    email varchar(300) not null check (email <> '')
    hash varchar(100) not null CHECK (hash <> '')
);

create table requests (
    id integer primary key generated always as identity,
    id integer not null,
    created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
    closed bool not null default false,
    user_id integer not null references users(id),
    label varchar(50) not null default '',
    data text not null default ''
    data text not null default '',
    referer text not null default '',
    unique (user_id, id)
);

CREATE OR REPLACE FUNCTION assign_request_id() RETURNS TRIGGER AS $$
BEGIN
    SELECT coalesce(max(id), 0) + 1 INTO NEW.id
    FROM requests
    WHERE user_id = NEW.user_id;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tr_assign_request_id
    BEFORE INSERT ON requests
    FOR EACH ROW EXECUTE PROCEDURE assign_request_id();
\ No newline at end of file

M storage/user.go => storage/user.go +8 -4
@@ 24,10 24,10 @@ func (u ErrUserExists) Error() string {
	return "user already exists"
}

const queryFindName = `SELECT id, name, hash, email FROM users WHERE name=lower($1);`
const queryFindName = `SELECT id, name, hash FROM users WHERE name=lower($1);`

func (s *Storage) queryUser(q string, params ...interface{}) (user model.User, err error) {
	err = s.db.QueryRow(q, params...).Scan(&user.Id, &user.Name, &user.Hash, &user.Email)
	err = s.db.QueryRow(q, params...).Scan(&user.Id, &user.Name, &user.Hash)
	return
}



@@ 53,7 53,7 @@ func (s *Storage) CreateUser(request model.UserCreationRequest) (int64, error) {
	if err != nil {
		return userId, err
	}
	if err := tx.QueryRowContext(ctx, `insert into users (name, hash, email) values (lower($1), $2, $3) returning id`, request.Name, string(hash), request.Email).Scan(&userId); err != nil {
	if err := tx.QueryRowContext(ctx, `insert into users (name, hash) values (lower($1), $2) returning id`, request.Name, string(hash)).Scan(&userId); err != nil {
		tx.Rollback()
		return userId, err
	}


@@ 62,7 62,11 @@ func (s *Storage) CreateUser(request model.UserCreationRequest) (int64, error) {
}

func (s *Storage) UserById(id int64) (model.User, error) {
	return s.queryUser(`SELECT id, name, hash, email FROM users WHERE id=$1;`, id)
	return s.queryUser(`SELECT id, name, hash FROM users WHERE id=$1;`, id)
}

func (s *Storage) UserBySecret(secret string) (model.User, error) {
	return s.queryUser(`SELECT id, name, hash FROM users WHERE hash=$1;`, secret)
}

func (s *Storage) VerifyUser(user model.User) (model.User, error) {

M web/handler/common.go => web/handler/common.go +2 -2
@@ 17,8 17,8 @@ var TplCommonMap = map[string]string{
<header>
    {{ if logged }}

    <a href="/">inbox</a>
    <a href="/?closed=true">archive</a>
    <a href="/">home</a>
    <a href="/feed.atom?s={{ .logged.Hash }}">feed</a>
    <a href="/manual">manual</a>
    {{ .logged.Name }} (<a href="/logout">logout</a>)


A web/handler/feed_show.go => web/handler/feed_show.go +140 -0
@@ 0,0 1,140 @@
package handler

import (
	"encoding/xml"
	"errors"
	"fmt"
	"net/http"
	"riku/model"
	"time"
)

type Feed struct {
	XMLName  xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
	Title    string   `xml:"title"`
	ID       string   `xml:"id"`
	Link     []Link   `xml:"link"`
	Updated  TimeStr  `xml:"updated"`
	Author   *Person  `xml:"author"`
	Icon     string   `xml:"icon,omitempty"`
	Logo     string   `xml:"logo,omitempty"`
	Subtitle string   `xml:"subtitle,omitempty"`
	Entry    []*Entry `xml:"entry"`
}

type Entry struct {
	Title     string  `xml:"title"`
	ID        string  `xml:"id"`
	Link      []Link  `xml:"link"`
	Published TimeStr `xml:"published"`
	Updated   TimeStr `xml:"updated"`
	Author    *Person `xml:"author"`
	Summary   *Text   `xml:"summary"`
	Content   *Text   `xml:"content"`
}

type Link struct {
	Rel      string `xml:"rel,attr,omitempty"`
	Href     string `xml:"href,attr"`
	Type     string `xml:"type,attr,omitempty"`
	HrefLang string `xml:"hreflang,attr,omitempty"`
	Title    string `xml:"title,attr,omitempty"`
	Length   uint   `xml:"length,attr,omitempty"`
}

type Person struct {
	Name     string `xml:"name"`
	URI      string `xml:"uri,omitempty"`
	Email    string `xml:"email,omitempty"`
	InnerXML string `xml:",innerxml"`
}

type Text struct {
	Type string `xml:"type,attr"`
	Body string `xml:",chardata"`
}

type TimeStr string

func Time(t time.Time) TimeStr {
	return TimeStr(t.Format("2006-01-02T15:04:05-07:00"))
}

func createAtomEntryFromResponse(response model.Request) *Entry {
	title := fmt.Sprintf("Response %d", response.Id)
	if response.Label != "" {
		title = fmt.Sprintf("[%s] %s", response.Label, title)
	}
	return &Entry{
		Title: title,
		ID:    fmt.Sprintf("https://riku.miso.town/response?id=%d", response.Id),
		Link: []Link{
			{
				Rel:  "alternate",
				Href: fmt.Sprintf("https://riku.miso.town/response?id=%d", response.Id),
				Type: "text/html",
			},
		},
		Updated:   Time(response.CreatedAt),
		Published: Time(response.CreatedAt),
		//Author: &Person{
		//	Name: status.User,
		//	URI:  fmt.Sprintf("https://status.cafe/users/%s", status.User),
		//},
		//Content: &Text{
		//	Type: "html",
		//	Body: status.ContentDisplay(),
		//},
	}
}

func (h *handler) showFeedView(w http.ResponseWriter, r *http.Request) {
	s := r.URL.Query().Get("s")
	user, err := h.store.UserBySecret(s)
	if err != nil {
		serverError(w, errors.New("Wrong URL"))
		return
	}

	feed := Feed{
		Title:    "Riku",
		ID:       "https://riku.miso.town/",
		Subtitle: "Your form responses",
		//Icon:     "/assets/icon.png",
		//Author: &Person{
		//	Name: "status.cafe",
		//	URI:  "https://status.cafe",
		//},
		Updated: Time(time.Now()),
		Link: []Link{
			{
				Rel:  "self",
				Href: "https://riku.miso.town/feed.atom",
			},
			{
				Rel:  "alternate",
				Type: "text/html",
				Href: "https://riku.miso.town",
			},
		},
	}

	responses, err := h.store.AllResponses(user.Id)
	if err != nil {
		serverError(w, err)
		return
	}

	for _, response := range responses {
		feed.Entry = append(feed.Entry, createAtomEntryFromResponse(response))
	}

	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Content-Type", "application/atom+xml")
	var data []byte
	data, err = xml.MarshalIndent(&feed, "", "    ")
	if err != nil {
		serverError(w, err)
	}
	w.Write([]byte(xml.Header + string(data)))
}

M web/handler/form/user.go => web/handler/form/user.go +0 -2
@@ 10,7 10,6 @@ type UserForm struct {
	Username string
	Password string
	Confirm  string
	Email    string
	Key      string
}



@@ 33,7 32,6 @@ func NewUserForm(r *http.Request) *UserForm {
		Username: r.FormValue("name"),
		Confirm:  r.FormValue("confirm"),
		Password: r.FormValue("password"),
		Email:    r.FormValue("email"),
		Key:      r.FormValue("key"),
	}
}

M web/handler/handler.go => web/handler/handler.go +11 -4
@@ 4,6 4,7 @@ import (
	"context"
	"fmt"
	"github.com/gorilla/mux"
	"log"
	"net/http"
	"riku/storage"
	"riku/web/handler/request"


@@ 19,6 20,11 @@ func list(closed, label string) string {
	return fmt.Sprintf("/?label=%s&closed=%s", label, closed)
}

func serverError(w http.ResponseWriter, err error) {
	log.Println("[server error]", err)
	http.Error(w, fmt.Sprintf("server error: %s", err), http.StatusInternalServerError)
}

func (h *handler) handleSessionMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		user, _ := h.session.GetUser(r)


@@ 51,14 57,15 @@ func New(store *storage.Storage, s *session.Manager) http.Handler {
	router.HandleFunc("/register", h.showRegisterView).Methods(http.MethodGet)
	router.HandleFunc("/register", h.register).Methods(http.MethodPost)
	router.HandleFunc("/submit", h.submitRequest).Methods(http.MethodPost)
	router.HandleFunc("/close", h.closeRequest).Methods(http.MethodGet)
	router.HandleFunc("/open", h.openRequest).Methods(http.MethodGet)
	router.HandleFunc("/", h.requestList).Methods(http.MethodGet)
	router.HandleFunc("/close", h.moveResponseToArchive).Methods(http.MethodGet)
	router.HandleFunc("/open", h.moveResponseToInbox).Methods(http.MethodGet)
	router.HandleFunc("/", h.responseList).Methods(http.MethodGet)
	router.HandleFunc("/delete", h.removeRequest).Methods(http.MethodGet)
	router.HandleFunc("/logout", h.logout).Methods(http.MethodGet)
	router.HandleFunc("/thank-you", h.showThankYouView).Methods(http.MethodGet)
	router.HandleFunc("/manual", h.showManualView).Methods(http.MethodGet)
	router.HandleFunc("/form", h.showFormView).Methods(http.MethodGet)
	router.HandleFunc("/response", h.showResponseView).Methods(http.MethodGet)
	router.HandleFunc("/feed.atom", h.showFeedView).Methods(http.MethodGet)

	return router
}

M web/handler/html.go => web/handler/html.go +102 -150
@@ 3,95 3,40 @@
package handler

var TplMap = map[string]string{
	"form": `{{ define "content" }}
<!--<nav class="tabs">-->
<!--    <a href="/?closed=false" {{ if not .form.Closed }}class="selected"{{ end }}>inbox</a> <a href="/?closed=true" {{ if .form.Closed }}class="selected"{{ end }}>archive</a>-->
<!--</nav>-->

<nav class="breadcrumb">
    <a href="/?closed={{ .form.Closed }}">{{ if .form.Closed }}archive{{ else }}inbox{{ end }}</a> › Form #{{ .form.Id }}
</nav>

<h1>Form #{{ .form.Id }}</h1>

<div>
<table>
    <tr>
        <td>Received</td>
        <td class="grow">{{ timeAgo .form.CreatedAt }}</td>
    </tr>
    <tr>
        <td>Label</td>
        <td class="grow">{{ .form.Label }}</td>
    </tr>
</table>

<div class="actions">
    {{ if .form.Closed }}
        <form action="/open" method="get" class="action">
            <input type="hidden" name="id" value="{{ .form.Id }}">
            <input type="submit" value="move to inbox">
        </form>
    {{ else }}
        <form action="/close" method="get" class="action">
            <input type="hidden" name="id" value="{{ .form.Id }}">
            <input type="submit" value="move to archive">
        </form>
    {{ end }}

    <form action="/delete" method="get" class="action">
        <input type="hidden" name="id" value="{{ .form.Id }}">
        <input type="hidden" name="closed" value="{{ .form.Closed }}">
        <input type="submit" value="delete">
    </form>
</div>



<dl>
    {{ range $key, $value := .form.Form }}
    <dt>{{ $key }}</dt>
    <dd>
        {{ range $value }}
        <p>{{ . }}</p>
        {{ end }}
    </dd>
    {{ end }}
</dl>
</div>
{{ end }}`,
	"index": `{{ define "title" }}Login{{ end }}

{{ define "content" }}
<h1>Add forms to your site</h1>
<h1>Collect responses from HTML forms</h1>

<p>Riku is a tiny service to process requests through web forms. For example:</p>
<p>Riku lets you collect responses from forms on your site. Forms to:</p>

<ul>
    <li>Request to join a mailing list</li>
    <li>To join a beta</li>
    <li>To exchange banners</li>
    <li>To join a webring</li>
    <li>To comment a blog post</li>
    <li>To contact yourself</li>
    <li> much more!</li>
    <li>Send you a comment</li>
    <li>Join your mailing list</li>
    <li>Exchange banners</li>
    <li>Join your webring</li>
    <li>Join your beta</li>
    <li>or anything you'd like!</li>
</ul>

<p>Riku provides a special URL to use on your forms:</p>
<p>Point your forms to Riku:</p>

<pre>&lt;form action=&quot;https://riku.miso.town/submit?user_id=1&quot; method=&quot;post&quot;&gt;
    &lt;label for=&quot;email&quot;&gt;Email:&lt;/label&gt;
    &lt;input type=&quot;email&quot; name=&quot;email&quot; id=&quot;email&quot; required&gt;
    &lt;input type=&quot;submit&quot; value=&quot;submit&quot;&gt;
    ...
&lt;/form&gt;
</pre>

<p>...and lets you manage all your requests through the web interface. Add your email to receive a key:</p>
<form action="https://riku.miso.town/submit?user_id=1&label=riku" method="post">
    <label for="email">Email:</label>
    <input type="email" name="email" id="email" required>
    <input type="submit" value="submit">
</form>
<p>...and retrieve all the responses on the web interface or via the RSS feed.</p>

<p>
    <a href="/register">Register</a><br>
    <a href="/manual">Read the manual</a>
</p>
<!--<form action="https://riku.miso.town/submit?user_id=1&label=riku" method="post">-->
<!--    <label for="email">Email:</label>-->
<!--    <input type="email" name="email" id="email" required>-->
<!--    <input type="submit" value="submit">-->
<!--</form>-->
{{ end }}`,
	"login": `{{ define "title" }}Login{{ end }}



@@ 118,9 63,9 @@ var TplMap = map[string]string{
{{ define "content" }}
<h1>Manual</h1>

<p>Riku lets you capture requests through HTML forms. Here's the manual.</p>
<p>Riku allows you to collect responses from HTML forms. To use Riku, you first need to have your own site. No need for PHP, Riku works with standard HTML. It doesn't even need JavaScript either. If you don't have a site yet, you can use <a href="https://ichi.city">ichi</a>. This manual will show you how to create a simple HTML form on your site, and collect all the responses on Riku. Are you ready? Let's get started!</p>

<h2>Submitting requests</h2>
<h2>Hello, form!</h2>
<p>Let's build a simple contact form for <a href="https://m15o.ichi.city">my site</a>. I would like the form to include a name, an optional email and a message field. In HTML, it would look like that:</p>

<pre>


@@ 145,7 90,9 @@ var TplMap = map[string]string{
&lt;/form&gt;
</pre>

<p>You can follow along by creating a local .html file or by creating a site on <a href="https://ichi.city">ichi</a>. You'll notice we haven't added an action URL to the form yet. When you login to Riku, you will be given your own URL that looks something like that:</p>
<h2>Dear form, send the responses to Riku</h2>

<p>You'll notice we haven't added an action URL to the form yet. When you login to Riku, you will be given your own URL that looks something like that:</p>

<pre>https://riku.miso.town/submit?user_id=1</pre>



@@ 159,7 106,7 @@ var TplMap = map[string]string{

<p>From now on, you will be able to see all the messages on Riku.</p>

<h3>Redirecting users</h3>
<h2>Redirecting users</h2>

<p>If you'd like your user to be shown a custom page when the form is submitted, you can add a special <b>redirect</b> field to the form:</p>



@@ 169,7 116,7 @@ var TplMap = map[string]string{

<p>Riku will redirect the user there once the form data is captured.</p>

<h3>Labeling requests</h3>
<h2>Labeling responses</h2>

<p>If you are like me and use Riku in many places, a good practice is to label your requests to help you triage them. You do it by appending a query string parameter to your Riku URL:</p>



@@ 177,10 124,6 @@ var TplMap = map[string]string{

<p>Now all requests coming from the contact form will be labeled with "contact".</p>

<h2>Managing requests</h2>

<p>All new requests coming to Riku are listed as "open". Once you've read them (and maybe acted on them), you can "close" them.</p>

{{ end }}`,
	"register": `{{ define "title" }}Register{{ end }}



@@ 189,6 132,7 @@ var TplMap = map[string]string{
{{ if .errorMessage }}
<p class="errors">{{ .errorMessage }}</p>
{{ end }}
<p>Use <a href="https://m15o.ichi.city/riku/key-request.html">this form</a> to get a key</p>
<form action="/register" method="post" class="auth-form">
    <div class="field">
        <label for="name">Username</label>


@@ 203,10 147,6 @@ var TplMap = map[string]string{
        <input type="password" id="confirm" name="confirm" required/>
    </div>
    <div class="field">
        <label for="email">Email</label>
        <input type="email" id="email" name="email" required/>
    </div>
    <div class="field">
        <label for="key">Key</label>
        <input type="text" id="key" name="key" required/>
    </div>


@@ 214,13 154,72 @@ var TplMap = map[string]string{
</form>
{{ end }}
`,
	"requests": `{{ define "content" }}
	"response": `{{ define "content" }}
<nav class="tabs">
    <a href="/?closed=false" {{ if not .response.Closed }}class="selected"{{ end }}>inbox</a> <a href="/?closed=true" {{ if .response.Closed }}class="selected"{{ end }}>archive</a>
</nav>

<!--<nav class="tabs">-->
<!--    <a href="?closed=false" {{ if not .closed }}class="selected"{{ end }}>inbox</a> <a href="?closed=true" {{ if .closed }}class="selected"{{ end }}>archive</a>-->
<!--</nav>-->
<nav class="breadcrumb">
    <a href="/?closed={{ .response.Closed }}">{{ if .response.Closed }}archive{{ else }}inbox{{ end }}</a> › Response #{{ .response.Id }}
</nav>

<h1>Response #{{ .response.Id }}</h1>

<div>
<table class="meta">
    <tr>
        <td>Received</td>
        <td class="grow">{{ timeAgo .response.CreatedAt }}</td>
    </tr>
    <tr>
        <td>Referer</td>
        <td class="grow">{{ .response.Referer }}</td>
    </tr>
    <tr>
        <td>Label</td>
        <td class="grow">{{ .response.Label }}</td>
    </tr>
</table>

<div class="actions">
    {{ if .response.Closed }}
        <form action="/open" method="get" class="action">
            <input type="hidden" name="id" value="{{ .response.Id }}">
            <input type="submit" value="move to inbox">
        </form>
    {{ else }}
        <form action="/close" method="get" class="action">
            <input type="hidden" name="id" value="{{ .response.Id }}">
            <input type="submit" value="move to archive">
        </form>
    {{ end }}

    <form action="/delete" method="get" class="action">
        <input type="hidden" name="id" value="{{ .response.Id }}">
        <input type="hidden" name="closed" value="{{ .response.Closed }}">
        <input type="submit" value="delete">
    </form>
</div>



<dl>
    {{ range $key, $value := .response.Form }}
    <dt>{{ $key }}</dt>
    <dd>
        {{ range $value }}
        <p>{{ . }}</p>
        {{ end }}
    </dd>
    {{ end }}
</dl>
</div>
{{ end }}`,
	"responses": `{{ define "content" }}

<nav class="tabs">
    <a href="?closed=false" {{ if not .closed }}class="selected"{{ end }}>inbox</a> <a href="?closed=true" {{ if .closed }}class="selected"{{ end }}>archive</a>
</nav>

{{ if not .closed }}
<h1>Inbox</h1>


@@ 237,73 236,26 @@ var TplMap = map[string]string{
</p>
{{ end }}

<!--<table>-->
<!--    <thead>-->
<!--    <tr>-->
<!--        <th>ID</th>-->
<!--        <th>Label</th>-->
<!--        <th>Request</th>-->
<!--&lt;!&ndash;        <th>Action</th>&ndash;&gt;-->
<!--    </tr>-->
<!--    </thead>-->
<!--    <tbody>-->
<!--    {{ range .requests }}-->
<!--    <tr>-->
<!--        <td style="text-align: center;">{{ .Id }}</a></td>-->
<!--        <td style="text-align: center;"><a href="?label={{ .Label }}&closed={{ $.closed }}">{{ .Label }}</a></td>-->
<!--        <td>-->
<!--            <a href="/form?id={{ .Id }}">Received on {{ iso8601Time .CreatedAt }}</a>-->
<!--&lt;!&ndash;            <div>&ndash;&gt;-->
<!--&lt;!&ndash;                <details>&ndash;&gt;-->
<!--&lt;!&ndash;                    <summary></summary>&ndash;&gt;-->
<!--&lt;!&ndash;                    <dl>&ndash;&gt;-->
<!--&lt;!&ndash;                        {{ range $key, $value := .Form }}&ndash;&gt;-->
<!--&lt;!&ndash;                        <dt>{{ $key }}</dt>&ndash;&gt;-->
<!--&lt;!&ndash;                        <dd>&ndash;&gt;-->
<!--&lt;!&ndash;                            {{ range $value }}&ndash;&gt;-->
<!--&lt;!&ndash;                            <p>{{ . }}</p>&ndash;&gt;-->
<!--&lt;!&ndash;                            {{ end }}&ndash;&gt;-->
<!--&lt;!&ndash;                        </dd>&ndash;&gt;-->
<!--&lt;!&ndash;                        {{ end }}&ndash;&gt;-->
<!--&lt;!&ndash;                    </dl>&ndash;&gt;-->
<!--&lt;!&ndash;                </details>&ndash;&gt;-->

<!--&lt;!&ndash;            </div>&ndash;&gt;-->
<!--        </td>-->
<!--&lt;!&ndash;        <td style="text-align: center;">&ndash;&gt;-->
<!--&lt;!&ndash;            {{ if .Closed }}&ndash;&gt;-->
<!--&lt;!&ndash;            <a href="/open?id={{ .Id }}&label={{ $.Label }}&closed={{ $.closed }}">move to inbox</a>&ndash;&gt;-->
<!--&lt;!&ndash;            {{ else }}&ndash;&gt;-->
<!--&lt;!&ndash;            <a href="/close?id={{ .Id }}&label={{ $.Label }}&closed={{ $.closed }}">archive</a>&ndash;&gt;-->
<!--&lt;!&ndash;            {{ end }}&ndash;&gt;-->
<!--&lt;!&ndash;        </td>&ndash;&gt;-->
<!--&lt;!&ndash;        <td style="text-align: center;"><a href="/delete?id={{ .Id }}&label={{ $.Label }}&closed={{ $.closed }}">Delete</a></td>&ndash;&gt;-->
<!--    </tr>-->
<!--    {{ end }}-->
<!--    </tbody>-->
<!--</table>-->

<table>
    <thead>
    <tr>
        <th>Label</th>
        <th>Received</th>
        <th>Response</th>
    </tr>
    </thead>
<table class="responses">
    <tbody>
    {{ range .requests }}
    <tr>
        <td style="text-align: center;"><a href="?label={{ .Label }}&closed={{ $.closed }}">{{ .Label }}</a></td>
        <td style="white-space: nowrap;">{{ timeAgo .CreatedAt }}</td>
        <td class="grow"><a href="/form?id={{ .Id }}">Response #{{ .Id }}</a></td>
        <td class="grow">
            <div class="response">
                <a href="/response?id={{ .Id }}">Response #{{ .Id }}</a>
                {{ if .Label }}
                <a href="?label={{ .Label }}&closed={{ $.closed }}">{{ .Label }}</a>
                {{ end }}
            </div>
        </td>
    </tr>
    {{ end }}
    </tbody>
</table>

{{ else }}
<p>No forms found.</p>
<p>No responses found.</p>
{{ end }}

{{ end }}`,

M web/handler/html/common/layout.html => web/handler/html/common/layout.html +2 -2
@@ 12,8 12,8 @@
<header>
    {{ if logged }}

    <a href="/">inbox</a>
    <a href="/?closed=true">archive</a>
    <a href="/">home</a>
    <a href="/feed.atom?s={{ .logged.Hash }}">feed</a>
    <a href="/manual">manual</a>
    {{ .logged.Name }} (<a href="/logout">logout</a>)


M web/handler/html/index.html => web/handler/html/index.html +21 -19
@@ 1,33 1,35 @@
{{ define "title" }}Login{{ end }}

{{ define "content" }}
<h1>Add forms to your site</h1>
<h1>Collect responses from HTML forms</h1>

<p>Riku is a tiny service to process requests through web forms. For example:</p>
<p>Riku lets you collect responses from forms on your site. Forms to:</p>

<ul>
    <li>Request to join a mailing list</li>
    <li>To join a beta</li>
    <li>To exchange banners</li>
    <li>To join a webring</li>
    <li>To comment a blog post</li>
    <li>To contact yourself</li>
    <li> much more!</li>
    <li>Send you a comment</li>
    <li>Join your mailing list</li>
    <li>Exchange banners</li>
    <li>Join your webring</li>
    <li>Join your beta</li>
    <li>or anything you'd like!</li>
</ul>

<p>Riku provides a special URL to use on your forms:</p>
<p>Point your forms to Riku:</p>

<pre>&lt;form action=&quot;https://riku.miso.town/submit?user_id=1&quot; method=&quot;post&quot;&gt;
    &lt;label for=&quot;email&quot;&gt;Email:&lt;/label&gt;
    &lt;input type=&quot;email&quot; name=&quot;email&quot; id=&quot;email&quot; required&gt;
    &lt;input type=&quot;submit&quot; value=&quot;submit&quot;&gt;
    ...
&lt;/form&gt;
</pre>

<p>...and lets you manage all your requests through the web interface. Add your email to receive a key:</p>
<form action="https://riku.miso.town/submit?user_id=1&label=riku" method="post">
    <label for="email">Email:</label>
    <input type="email" name="email" id="email" required>
    <input type="submit" value="submit">
</form>
<p>...and retrieve all the responses on the web interface or via the RSS feed.</p>

<p>
    <a href="/register">Register</a><br>
    <a href="/manual">Read the manual</a>
</p>
<!--<form action="https://riku.miso.town/submit?user_id=1&label=riku" method="post">-->
<!--    <label for="email">Email:</label>-->
<!--    <input type="email" name="email" id="email" required>-->
<!--    <input type="submit" value="submit">-->
<!--</form>-->
{{ end }}
\ No newline at end of file

M web/handler/html/manual.html => web/handler/html/manual.html +7 -9
@@ 3,9 3,9 @@
{{ define "content" }}
<h1>Manual</h1>

<p>Riku lets you capture requests through HTML forms. Here's the manual.</p>
<p>Riku allows you to collect responses from HTML forms. To use Riku, you first need to have your own site. No need for PHP, Riku works with standard HTML. It doesn't even need JavaScript either. If you don't have a site yet, you can use <a href="https://ichi.city">ichi</a>. This manual will show you how to create a simple HTML form on your site, and collect all the responses on Riku. Are you ready? Let's get started!</p>

<h2>Submitting requests</h2>
<h2>Hello, form!</h2>
<p>Let's build a simple contact form for <a href="https://m15o.ichi.city">my site</a>. I would like the form to include a name, an optional email and a message field. In HTML, it would look like that:</p>

<pre>


@@ 30,7 30,9 @@
&lt;/form&gt;
</pre>

<p>You can follow along by creating a local .html file or by creating a site on <a href="https://ichi.city">ichi</a>. You'll notice we haven't added an action URL to the form yet. When you login to Riku, you will be given your own URL that looks something like that:</p>
<h2>Dear form, send the responses to Riku</h2>

<p>You'll notice we haven't added an action URL to the form yet. When you login to Riku, you will be given your own URL that looks something like that:</p>

<pre>https://riku.miso.town/submit?user_id=1</pre>



@@ 44,7 46,7 @@

<p>From now on, you will be able to see all the messages on Riku.</p>

<h3>Redirecting users</h3>
<h2>Redirecting users</h2>

<p>If you'd like your user to be shown a custom page when the form is submitted, you can add a special <b>redirect</b> field to the form:</p>



@@ 54,7 56,7 @@

<p>Riku will redirect the user there once the form data is captured.</p>

<h3>Labeling requests</h3>
<h2>Labeling responses</h2>

<p>If you are like me and use Riku in many places, a good practice is to label your requests to help you triage them. You do it by appending a query string parameter to your Riku URL:</p>



@@ 62,8 64,4 @@

<p>Now all requests coming from the contact form will be labeled with "contact".</p>

<h2>Managing requests</h2>

<p>All new requests coming to Riku are listed as "open". Once you've read them (and maybe acted on them), you can "close" them.</p>

{{ end }}
\ No newline at end of file

M web/handler/html/register.html => web/handler/html/register.html +1 -4
@@ 5,6 5,7 @@
{{ if .errorMessage }}
<p class="errors">{{ .errorMessage }}</p>
{{ end }}
<p>Use <a href="https://m15o.ichi.city/riku/key-request.html">this form</a> to get a key</p>
<form action="/register" method="post" class="auth-form">
    <div class="field">
        <label for="name">Username</label>


@@ 19,10 20,6 @@
        <input type="password" id="confirm" name="confirm" required/>
    </div>
    <div class="field">
        <label for="email">Email</label>
        <input type="email" id="email" name="email" required/>
    </div>
    <div class="field">
        <label for="key">Key</label>
        <input type="text" id="key" name="key" required/>
    </div>

D web/handler/html/requests.html => web/handler/html/requests.html +0 -93
@@ 1,93 0,0 @@
{{ define "content" }}

<!--<nav class="tabs">-->
<!--    <a href="?closed=false" {{ if not .closed }}class="selected"{{ end }}>inbox</a> <a href="?closed=true" {{ if .closed }}class="selected"{{ end }}>archive</a>-->
<!--</nav>-->



{{ if not .closed }}
<h1>Inbox</h1>
<div class="info"><p>POST to https://riku.miso.town/submit?user_id={{ .logged.Id }}</p></div>
{{ else }}
<h1>Archive</h1>
{{ end }}

{{ if .requests }}

{{ if gt (len .labels) 1 }}
<p>
    <a href="/?closed={{ .closed }}">all</a>{{ range .labels }} {{ if eq . $.label }}{{ . }}{{ else }}<a href="?label={{ . }}&closed={{ $.closed }}">{{ . }}</a>{{ end }}{{ end }}
</p>
{{ end }}

<!--<table>-->
<!--    <thead>-->
<!--    <tr>-->
<!--        <th>ID</th>-->
<!--        <th>Label</th>-->
<!--        <th>Request</th>-->
<!--&lt;!&ndash;        <th>Action</th>&ndash;&gt;-->
<!--    </tr>-->
<!--    </thead>-->
<!--    <tbody>-->
<!--    {{ range .requests }}-->
<!--    <tr>-->
<!--        <td style="text-align: center;">{{ .Id }}</a></td>-->
<!--        <td style="text-align: center;"><a href="?label={{ .Label }}&closed={{ $.closed }}">{{ .Label }}</a></td>-->
<!--        <td>-->
<!--            <a href="/form?id={{ .Id }}">Received on {{ iso8601Time .CreatedAt }}</a>-->
<!--&lt;!&ndash;            <div>&ndash;&gt;-->
<!--&lt;!&ndash;                <details>&ndash;&gt;-->
<!--&lt;!&ndash;                    <summary></summary>&ndash;&gt;-->
<!--&lt;!&ndash;                    <dl>&ndash;&gt;-->
<!--&lt;!&ndash;                        {{ range $key, $value := .Form }}&ndash;&gt;-->
<!--&lt;!&ndash;                        <dt>{{ $key }}</dt>&ndash;&gt;-->
<!--&lt;!&ndash;                        <dd>&ndash;&gt;-->
<!--&lt;!&ndash;                            {{ range $value }}&ndash;&gt;-->
<!--&lt;!&ndash;                            <p>{{ . }}</p>&ndash;&gt;-->
<!--&lt;!&ndash;                            {{ end }}&ndash;&gt;-->
<!--&lt;!&ndash;                        </dd>&ndash;&gt;-->
<!--&lt;!&ndash;                        {{ end }}&ndash;&gt;-->
<!--&lt;!&ndash;                    </dl>&ndash;&gt;-->
<!--&lt;!&ndash;                </details>&ndash;&gt;-->

<!--&lt;!&ndash;            </div>&ndash;&gt;-->
<!--        </td>-->
<!--&lt;!&ndash;        <td style="text-align: center;">&ndash;&gt;-->
<!--&lt;!&ndash;            {{ if .Closed }}&ndash;&gt;-->
<!--&lt;!&ndash;            <a href="/open?id={{ .Id }}&label={{ $.Label }}&closed={{ $.closed }}">move to inbox</a>&ndash;&gt;-->
<!--&lt;!&ndash;            {{ else }}&ndash;&gt;-->
<!--&lt;!&ndash;            <a href="/close?id={{ .Id }}&label={{ $.Label }}&closed={{ $.closed }}">archive</a>&ndash;&gt;-->
<!--&lt;!&ndash;            {{ end }}&ndash;&gt;-->
<!--&lt;!&ndash;        </td>&ndash;&gt;-->
<!--&lt;!&ndash;        <td style="text-align: center;"><a href="/delete?id={{ .Id }}&label={{ $.Label }}&closed={{ $.closed }}">Delete</a></td>&ndash;&gt;-->
<!--    </tr>-->
<!--    {{ end }}-->
<!--    </tbody>-->
<!--</table>-->

<table>
    <thead>
    <tr>
        <th>Label</th>
        <th>Received</th>
        <th>Response</th>
    </tr>
    </thead>
    <tbody>
    {{ range .requests }}
    <tr>
        <td style="text-align: center;"><a href="?label={{ .Label }}&closed={{ $.closed }}">{{ .Label }}</a></td>
        <td style="white-space: nowrap;">{{ timeAgo .CreatedAt }}</td>
        <td class="grow"><a href="/form?id={{ .Id }}">Response #{{ .Id }}</a></td>
    </tr>
    {{ end }}
    </tbody>
</table>

{{ else }}
<p>No forms found.</p>
{{ end }}

{{ end }}
\ No newline at end of file

R web/handler/html/form.html => web/handler/html/response.html +18 -14
@@ 1,42 1,46 @@
{{ define "content" }}
<!--<nav class="tabs">-->
<!--    <a href="/?closed=false" {{ if not .form.Closed }}class="selected"{{ end }}>inbox</a> <a href="/?closed=true" {{ if .form.Closed }}class="selected"{{ end }}>archive</a>-->
<!--</nav>-->
<nav class="tabs">
    <a href="/?closed=false" {{ if not .response.Closed }}class="selected"{{ end }}>inbox</a> <a href="/?closed=true" {{ if .response.Closed }}class="selected"{{ end }}>archive</a>
</nav>

<nav class="breadcrumb">
    <a href="/?closed={{ .form.Closed }}">{{ if .form.Closed }}archive{{ else }}inbox{{ end }}</a> › Form #{{ .form.Id }}
    <a href="/?closed={{ .response.Closed }}">{{ if .response.Closed }}archive{{ else }}inbox{{ end }}</a> › Response #{{ .response.Id }}
</nav>

<h1>Form #{{ .form.Id }}</h1>
<h1>Response #{{ .response.Id }}</h1>

<div>
<table>
<table class="meta">
    <tr>
        <td>Received</td>
        <td class="grow">{{ timeAgo .form.CreatedAt }}</td>
        <td class="grow">{{ timeAgo .response.CreatedAt }}</td>
    </tr>
    <tr>
        <td>Referer</td>
        <td class="grow">{{ .response.Referer }}</td>
    </tr>
    <tr>
        <td>Label</td>
        <td class="grow">{{ .form.Label }}</td>
        <td class="grow">{{ .response.Label }}</td>
    </tr>
</table>

<div class="actions">
    {{ if .form.Closed }}
    {{ if .response.Closed }}
        <form action="/open" method="get" class="action">
            <input type="hidden" name="id" value="{{ .form.Id }}">
            <input type="hidden" name="id" value="{{ .response.Id }}">
            <input type="submit" value="move to inbox">
        </form>
    {{ else }}
        <form action="/close" method="get" class="action">
            <input type="hidden" name="id" value="{{ .form.Id }}">
            <input type="hidden" name="id" value="{{ .response.Id }}">
            <input type="submit" value="move to archive">
        </form>
    {{ end }}

    <form action="/delete" method="get" class="action">
        <input type="hidden" name="id" value="{{ .form.Id }}">
        <input type="hidden" name="closed" value="{{ .form.Closed }}">
        <input type="hidden" name="id" value="{{ .response.Id }}">
        <input type="hidden" name="closed" value="{{ .response.Closed }}">
        <input type="submit" value="delete">
    </form>
</div>


@@ 44,7 48,7 @@


<dl>
    {{ range $key, $value := .form.Form }}
    {{ range $key, $value := .response.Form }}
    <dt>{{ $key }}</dt>
    <dd>
        {{ range $value }}

A web/handler/html/responses.html => web/handler/html/responses.html +44 -0
@@ 0,0 1,44 @@
{{ define "content" }}

<nav class="tabs">
    <a href="?closed=false" {{ if not .closed }}class="selected"{{ end }}>inbox</a> <a href="?closed=true" {{ if .closed }}class="selected"{{ end }}>archive</a>
</nav>

{{ if not .closed }}
<h1>Inbox</h1>
<div class="info"><p>POST to https://riku.miso.town/submit?user_id={{ .logged.Id }}</p></div>
{{ else }}
<h1>Archive</h1>
{{ end }}

{{ if .requests }}

{{ if gt (len .labels) 1 }}
<p>
    <a href="/?closed={{ .closed }}">all</a>{{ range .labels }} {{ if eq . $.label }}{{ . }}{{ else }}<a href="?label={{ . }}&closed={{ $.closed }}">{{ . }}</a>{{ end }}{{ end }}
</p>
{{ end }}

<table class="responses">
    <tbody>
    {{ range .requests }}
    <tr>
        <td style="white-space: nowrap;">{{ timeAgo .CreatedAt }}</td>
        <td class="grow">
            <div class="response">
                <a href="/response?id={{ .Id }}">Response #{{ .Id }}</a>
                {{ if .Label }}
                <a href="?label={{ .Label }}&closed={{ $.closed }}">{{ .Label }}</a>
                {{ end }}
            </div>
        </td>
    </tr>
    {{ end }}
    </tbody>
</table>

{{ else }}
<p>No responses found.</p>
{{ end }}

{{ end }}
\ No newline at end of file

M web/handler/register.go => web/handler/register.go +0 -1
@@ 23,7 23,6 @@ func (h *handler) register(w http.ResponseWriter, r *http.Request) {
	userCreationRequest := model.UserCreationRequest{
		Name:     userForm.Username,
		Password: userForm.Password,
		Email:    userForm.Email,
	}

	if err := validator.ValidateUserCreation(h.store, userForm.Key, userCreationRequest); err != nil {

M web/handler/request_submit.go => web/handler/request_submit.go +1 -1
@@ 27,7 27,7 @@ func (h *handler) submitRequest(w http.ResponseWriter, r *http.Request) {

	data := string(v)

	err = h.store.Save(userId, label, data)
	err = h.store.Save(userId, r.Referer(), label, data)
	if err != nil {
		fmt.Println(err)
		return

R web/handler/request_list.go => web/handler/response_list.go +3 -3
@@ 6,7 6,7 @@ import (
	"riku/web/handler/request"
)

func (h *handler) requestList(w http.ResponseWriter, r *http.Request) {
func (h *handler) responseList(w http.ResponseWriter, r *http.Request) {
	user := request.GetUserContextKey(r)

	if user.Id == 0 {


@@ 18,7 18,7 @@ func (h *handler) requestList(w http.ResponseWriter, r *http.Request) {
	label := r.URL.Query().Get("label")
	closed := r.URL.Query().Get("closed") == "true"

	requests, err := h.store.Requests(user.Id, label, closed)
	requests, err := h.store.Responses(user.Id, label, closed)
	if err != nil {
		fmt.Println(err)
	}


@@ 28,7 28,7 @@ func (h *handler) requestList(w http.ResponseWriter, r *http.Request) {
		fmt.Println(err)
	}

	v := NewView(w, r, "requests")
	v := NewView(w, r, "responses")
	v.Set("requests", requests)
	v.Set("labels", labels)
	v.Set("label", label)

R web/handler/request_close.go => web/handler/response_move_archive.go +2 -3
@@ 1,13 1,12 @@
package handler

import (
	"fmt"
	"net/http"
	"riku/web/handler/request"
	"strconv"
)

func (h *handler) closeRequest(w http.ResponseWriter, r *http.Request) {
func (h *handler) moveResponseToArchive(w http.ResponseWriter, r *http.Request) {
	user := request.GetUserContextKey(r)

	id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)


@@ 16,5 15,5 @@ func (h *handler) closeRequest(w http.ResponseWriter, r *http.Request) {

	//status, label := r.URL.Query().Get("status"), r.URL.Query().Get("label")

	http.Redirect(w, r, fmt.Sprintf("/form?id=%d", id), http.StatusFound)
	http.Redirect(w, r, "/?closed=false", http.StatusFound)
}

R web/handler/request_open.go => web/handler/response_move_inbox.go +2 -3
@@ 1,13 1,12 @@
package handler

import (
	"fmt"
	"net/http"
	"riku/web/handler/request"
	"strconv"
)

func (h *handler) openRequest(w http.ResponseWriter, r *http.Request) {
func (h *handler) moveResponseToInbox(w http.ResponseWriter, r *http.Request) {
	user := request.GetUserContextKey(r)

	id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)


@@ 16,5 15,5 @@ func (h *handler) openRequest(w http.ResponseWriter, r *http.Request) {

	//closed, label := r.URL.Query().Get("closed"), r.URL.Query().Get("label")

	http.Redirect(w, r, fmt.Sprintf("/form?id=%d", id), http.StatusFound)
	http.Redirect(w, r, "/?closed=true", http.StatusFound)
}

R web/handler/form_show.go => web/handler/response_show.go +4 -4
@@ 7,17 7,17 @@ import (
	"strconv"
)

func (h *handler) showFormView(w http.ResponseWriter, r *http.Request) {
func (h *handler) showResponseView(w http.ResponseWriter, r *http.Request) {
	user := request.GetUserContextKey(r)

	id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)

	form, err := h.store.RequestById(user.Id, id)
	form, err := h.store.ResponseById(user.Id, id)
	if err != nil {
		fmt.Println(err)
	}

	v := NewView(w, r, "form")
	v.Set("form", form)
	v := NewView(w, r, "response")
	v.Set("response", form)
	v.Render()
}