Browse Source

workspace and login working

bmallred 9 years ago
parent
commit
49a70b2f81
12 changed files with 511 additions and 99 deletions
  1. 1 0
      .gitignore
  2. 61 6
      account/account.go
  3. 17 0
      account/account_test.go
  4. 6 0
      handlers/blog.go
  5. 200 1
      handlers/content.go
  6. 15 5
      handlers/logon.go
  7. 13 3
      html/article.html
  8. 4 4
      html/default.html
  9. 174 73
      html/workspace.html
  10. BIN
      loop
  11. 11 3
      main.go
  12. 9 4
      security/security_test.go

+ 1 - 0
.gitignore

21
21
22
*.exe
22
*.exe
23
loop
23
loop
24
loop_users

+ 61 - 6
account/account.go

16
16
17
const (
17
const (
18
	UserFile          = "loop_users"
18
	UserFile          = "loop_users"
19
	publishDateLayout = "200601021504"
19
	PublishDateLayout = "200601021504"
20
)
20
)
21
21
22
type Account struct {
22
type Account struct {
204
204
205
	// Create new content directory if necessary
205
	// Create new content directory if necessary
206
	if content == "" {
206
	if content == "" {
207
		content = fmt.Sprintf("%x", security.Hash(userDir+time.Now().Format(publishDateLayout), environment.Salt()))
207
		content = fmt.Sprintf("%x", security.Hash(userDir+time.Now().Format(PublishDateLayout), environment.Salt()))
208
	}
208
	}
209
209
210
	// Create new content directory
210
	// Create new content directory
218
218
219
	// Check if filename already exists
219
	// Check if filename already exists
220
	filePath := contentPath + s + filename
220
	filePath := contentPath + s + filename
221
	if _, err := os.Stat(filePath); os.IsExist(err) {
222
		return filePath, err
223
	}
221
	//if _, err := os.Stat(filePath); os.IsExist(err) {
222
	//	return filePath, err
223
	//}
224
224
225
	// Write file to content directory
225
	// Write file to content directory
226
	fi, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
226
	fi, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
250
	userDir := strings.ToLower(a.Username)
250
	userDir := strings.ToLower(a.Username)
251
	s := string(filepath.Separator)
251
	s := string(filepath.Separator)
252
	contentPath := baseDir + s + userDir + s + "content" + s + content
252
	contentPath := baseDir + s + userDir + s + "content" + s + content
253
	publishPath := baseDir + s + userDir + s + "blog" + s + date.Format(publishDateLayout) + "-" + prettyTitle
253
	publishPath := baseDir + s + userDir + s + "blog" + s + date.Format(PublishDateLayout) + "-" + prettyTitle
254
254
255
	// Check if the content exists
255
	// Check if the content exists
256
	if _, err := os.Stat(contentPath); os.IsNotExist(err) {
256
	if _, err := os.Stat(contentPath); os.IsNotExist(err) {
270
270
271
	return nil
271
	return nil
272
}
272
}
273
274
func (a *Account) Content() ([]string, error) {
275
	baseDir := os.Getenv("LOOP_DATA")
276
	userDir := strings.ToLower(a.Username)
277
	s := string(filepath.Separator)
278
	contentPath := baseDir + s + userDir + s + "content"
279
280
	d, err := os.Open(contentPath)
281
	if err != nil {
282
		return []string{}, err
283
	}
284
285
	return d.Readdirnames(0)
286
}
287
288
func (a *Account) Read(title string) (string, error) {
289
	baseDir := os.Getenv("LOOP_DATA")
290
	userDir := strings.ToLower(a.Username)
291
	s := string(filepath.Separator)
292
	fp := baseDir + s + userDir + s + "content" + s + title + s + "document.md"
293
294
	// Check to see if the file exists
295
	if _, err := os.Stat(fp); os.IsNotExist(err) {
296
		return "", err
297
	}
298
299
	// Open the file
300
	fi, err := os.Open(fp)
301
	if err != nil {
302
		return "", err
303
	}
304
305
	// Read all of its contents
306
	raw, err := ioutil.ReadAll(fi)
307
	if err != nil {
308
		return "", err
309
	}
310
311
	return string(raw), nil
312
}
313
314
func (a *Account) Remove(title string) error {
315
	baseDir := os.Getenv("LOOP_DATA")
316
	userDir := strings.ToLower(a.Username)
317
	s := string(filepath.Separator)
318
	fp := baseDir + s + userDir + s + "content" + s + title
319
320
	// Check to see if the file exists
321
	if _, err := os.Stat(fp); os.IsNotExist(err) {
322
		return err
323
	}
324
325
	// Delete the directory
326
	return os.RemoveAll(fp)
327
}

+ 17 - 0
account/account_test.go

72
	}
72
	}
73
}
73
}
74
74
75
func TestContent(t *testing.T) {
76
	a := Account{
77
		Username:   "guest",
78
		Passphrase: "guest",
79
	}
80
81
	environment.ConfigureEnvironment()
82
	a.Add("", "README.md", []byte{})
83
	c, err := a.Content()
84
	if err != nil {
85
		t.Error(err)
86
	}
87
	if len(c) == 0 {
88
		t.Error("No content found")
89
	}
90
}
91
75
func TestPublish(t *testing.T) {
92
func TestPublish(t *testing.T) {
76
	a := Account{
93
	a := Account{
77
		Username:   "guest",
94
		Username:   "guest",

+ 6 - 0
handlers/blog.go

16
	))
16
	))
17
)
17
)
18
18
19
type PageBlog struct {
20
	Title     string
21
	Published string
22
	Body      template.HTML
23
}
24
19
func BlogHandler(w http.ResponseWriter, r *http.Request) {
25
func BlogHandler(w http.ResponseWriter, r *http.Request) {
20
	if err := templateBlog.Execute(w, nil); err != nil {
26
	if err := templateBlog.Execute(w, nil); err != nil {
21
		http.Error(w, err.Error(), http.StatusInternalServerError)
27
		http.Error(w, err.Error(), http.StatusInternalServerError)

+ 200 - 1
handlers/content.go

1
package handlers
1
package handlers
2
2
3
import (
3
import (
4
	"errors"
4
	"html/template"
5
	"html/template"
5
	"log"
6
	"log"
6
	"net/http"
7
	"net/http"
8
	"time"
9
10
	"code.revolvingcow.com/revolvingcow/loop/account"
11
	"code.revolvingcow.com/revolvingcow/loop/environment"
12
	"code.revolvingcow.com/revolvingcow/loop/markdown"
13
	"code.revolvingcow.com/revolvingcow/loop/security"
14
15
	"github.com/gorilla/mux"
16
	"github.com/gorilla/sessions"
7
)
17
)
8
18
9
var (
19
var (
11
		"html/loop.html",
21
		"html/loop.html",
12
		"html/workspace.html",
22
		"html/workspace.html",
13
	))
23
	))
24
	store = sessions.NewCookieStore([]byte(environment.Salt()))
14
)
25
)
15
26
27
// WorkspaceModel passes information to the template engine providing context to the web page.
28
type WorkspaceModel struct {
29
	Username    string
30
	Directories []string
31
	Title       string
32
	Content     string
33
}
34
35
// ContentHandler is the default handler used for user content.
16
func ContentHandler(w http.ResponseWriter, r *http.Request) {
36
func ContentHandler(w http.ResponseWriter, r *http.Request) {
17
	log.Println("Serving template for the content handler")
37
	log.Println("Serving template for the content handler")
18
	if err := templateWorkspace.Execute(w, nil); err != nil {
38
39
	session, _ := store.Get(r, "session-account")
40
	account, err := validateCredentials(session)
41
	if err != nil {
42
		http.Error(w, err.Error(), http.StatusUnauthorized)
43
		return
44
	}
45
46
	directories, _ := account.Content()
47
	model := WorkspaceModel{
48
		Username:    account.Username,
49
		Directories: directories,
50
	}
51
52
	if err := templateWorkspace.Execute(w, model); err != nil {
19
		http.Error(w, err.Error(), http.StatusInternalServerError)
53
		http.Error(w, err.Error(), http.StatusInternalServerError)
54
		return
20
	}
55
	}
21
}
56
}
57
58
// ContentCreateHandler creates new content in the user context.
59
func ContentCreateHandler(w http.ResponseWriter, r *http.Request) {
60
	log.Println("Creating new document")
61
62
	session, _ := store.Get(r, "session-account")
63
	account, err := validateCredentials(session)
64
	if err != nil {
65
		http.Error(w, err.Error(), http.StatusUnauthorized)
66
		return
67
	}
68
69
	vars := mux.Vars(r)
70
	title := vars["title"]
71
	if title != "" {
72
		account.Add(title, "document.md", []byte{})
73
	} else {
74
		http.Redirect(w, r, account.Username, http.StatusSeeOther)
75
	}
76
77
	http.Redirect(w, r, "/"+account.Username+"/edit/"+title, http.StatusSeeOther)
78
}
79
80
// ContentReadHandler reads a document in the user context.
81
func ContentReadHandler(w http.ResponseWriter, r *http.Request) {
82
	log.Println("Reading document")
83
84
	session, _ := store.Get(r, "session-account")
85
	account, err := validateCredentials(session)
86
	if err != nil {
87
		http.Error(w, err.Error(), http.StatusUnauthorized)
88
		return
89
	}
90
91
	vars := mux.Vars(r)
92
	title := vars["title"]
93
	if title == "" {
94
		http.Error(w, "No title given", http.StatusInternalServerError)
95
		return
96
	}
97
98
	contents, err := account.Read(title)
99
	if err != nil {
100
		http.Error(w, err.Error(), http.StatusInternalServerError)
101
		return
102
	}
103
104
	directories, _ := account.Content()
105
	model := WorkspaceModel{
106
		Username:    account.Username,
107
		Directories: directories,
108
		Title:       title,
109
		Content:     contents,
110
	}
111
112
	if err := templateWorkspace.Execute(w, model); err != nil {
113
		http.Error(w, err.Error(), http.StatusInternalServerError)
114
		return
115
	}
116
}
117
118
// ContentPreviewHandler displays a Markdown document in HTML format.
119
func ContentPreviewHandler(w http.ResponseWriter, r *http.Request) {
120
	log.Println("Previewing document")
121
122
	session, _ := store.Get(r, "session-account")
123
	account, err := validateCredentials(session)
124
	if err != nil {
125
		http.Error(w, err.Error(), http.StatusUnauthorized)
126
		return
127
	}
128
129
	vars := mux.Vars(r)
130
	title := vars["title"]
131
	contents := r.FormValue("contents")
132
	if contents == "" {
133
		contents, _ = account.Read(title)
134
	}
135
	md := template.HTML(string(markdown.MarkdownToHtml([]byte(contents))))
136
	model := PageBlog{
137
		Title:     title,
138
		Published: time.Now().Format("2006-01-02 15:04"),
139
		Body:      md,
140
	}
141
142
	if err := templateArticle.Execute(w, model); err != nil {
143
		http.Error(w, err.Error(), http.StatusInternalServerError)
144
		return
145
	}
146
}
147
148
// ContentUpdateHandler updates content in the user context.
149
func ContentUpdateHandler(w http.ResponseWriter, r *http.Request) {
150
	log.Println("Updating document")
151
152
	session, _ := store.Get(r, "session-account")
153
	account, err := validateCredentials(session)
154
	if err != nil {
155
		http.Error(w, err.Error(), http.StatusUnauthorized)
156
		return
157
	}
158
159
	vars := mux.Vars(r)
160
	title := vars["title"]
161
	if title == "" {
162
		http.Error(w, "No title given", http.StatusInternalServerError)
163
		return
164
	}
165
166
	contents := r.FormValue("contents")
167
	if contents == "" {
168
		http.Error(w, "No content given", http.StatusInternalServerError)
169
		return
170
	}
171
172
	account.Add(title, "document.md", []byte(contents))
173
}
174
175
// ContentUploadHandler allows for multiple document uploads in the user context.
176
func ContentUploadHandler(w http.ResponseWriter, r *http.Request) {
177
	log.Println("Uploading attachment")
178
}
179
180
// ContentDeleteHandler deletes content in the user context.
181
func ContentDeleteHandler(w http.ResponseWriter, r *http.Request) {
182
	log.Println("Deleting document")
183
184
	session, _ := store.Get(r, "session-account")
185
	account, err := validateCredentials(session)
186
	if err != nil {
187
		http.Error(w, err.Error(), http.StatusUnauthorized)
188
		return
189
	}
190
191
	vars := mux.Vars(r)
192
	title := vars["title"]
193
	if title == "" {
194
		http.Error(w, "No title given", http.StatusInternalServerError)
195
		return
196
	}
197
198
	account.Remove(title)
199
	http.Redirect(w, r, "/"+account.Username, http.StatusSeeOther)
200
}
201
202
// validateCredentials attempts to validate session information.
203
func validateCredentials(session *sessions.Session) (account.Account, error) {
204
	user := session.Values["username"]
205
	pass := session.Values["passphrase"]
206
	if user == nil || pass == nil {
207
		return account.Account{}, errors.New("No session state")
208
	}
209
210
	account := account.Account{
211
		Username:   user.(string),
212
		Passphrase: security.GeneratePassphrase(user.(string), pass.(string)),
213
	}
214
	valid, err := account.Validate()
215
	if err != nil || !valid {
216
		return account, errors.New("Invalid credentials")
217
	}
218
219
	return account, nil
220
}

+ 15 - 5
handlers/logon.go

1
package handlers
1
package handlers
2
2
3
import (
3
import (
4
	"log"
4
	"net/http"
5
	"net/http"
5
6
6
	"code.revolvingcow.com/revolvingcow/loop/account"
7
	"code.revolvingcow.com/revolvingcow/loop/account"
8
	"code.revolvingcow.com/revolvingcow/loop/security"
7
)
9
)
8
10
11
// LogonHandler is responsible for the initial log on procedures.
9
func LogonHandler(w http.ResponseWriter, r *http.Request) {
12
func LogonHandler(w http.ResponseWriter, r *http.Request) {
10
	email := r.FormValue("email")
11
	password := r.FormValue("password")
13
	log.Println("Logon handler")
14
15
	login := r.FormValue("login")
16
	passphrase := r.FormValue("passphrase")
12
	a := account.Account{
17
	a := account.Account{
13
		Username:   email,
14
		Passphrase: password,
18
		Username:   login,
19
		Passphrase: security.GeneratePassphrase(login, passphrase),
15
	}
20
	}
16
21
17
	err := a.Create()
22
	err := a.Create()
18
	if err != nil {
23
	if err != nil {
19
		if err.Error() == "Account already exists" {
24
		if err.Error() != "Account already exists" {
20
			http.Error(w, err.Error(), http.StatusInternalServerError)
25
			http.Error(w, err.Error(), http.StatusInternalServerError)
21
			return
26
			return
22
		}
27
		}
23
	}
28
	}
24
29
30
	session, _ := store.Get(r, "session-account")
31
	session.Values["username"] = login
32
	session.Values["passphrase"] = passphrase
33
	store.Save(r, w, session)
34
25
	http.Redirect(w, r, "/"+a.Username, http.StatusSeeOther)
35
	http.Redirect(w, r, "/"+a.Username, http.StatusSeeOther)
26
}
36
}

+ 13 - 3
html/article.html

1
{{ define "title" }}{{ end }}
1
{{ define "title" }}{{ end }}
2
{{ define "styles" }}{{ end }}
2
{{ define "styles" }}
3
    <style type="text/css">
4
        h3 small { display: block; font-size: 45%; color: #666; }
5
        #markdown ul { padding-left: 2em; }
6
        #markdown ul li { list-style-type: initial; }
7
        #markdown code { padding: 1em; background-color: #eee; }
8
    </style>
9
{{ end }}
3
{{ define "content" }}
10
{{ define "content" }}
4
    <article>
11
    <article>
5
        <h3>
12
        <h3>
6
            {{ .Title }}
13
            {{ .Title }}
7
            <small>{{ .Published }}</small>
14
            <small>Published on {{ .Published }}</small>
15
            <hr />
8
        </h3>
16
        </h3>
9
        {{ .Body }}
17
        <div id="markdown">
18
            {{ .Body }}
19
        </div>
10
    </article>
20
    </article>
11
{{ end }}
21
{{ end }}
12
{{ define "scripts" }}{{ end }}
22
{{ define "scripts" }}{{ end }}

+ 4 - 4
html/default.html

150
                    <form class="col s12" method="post" action="/logon">
150
                    <form class="col s12" method="post" action="/logon">
151
                        <div class="row">
151
                        <div class="row">
152
                            <div class="input-field col s12">
152
                            <div class="input-field col s12">
153
                                <input id="email" type="text" class="validate">
154
                                <label for="email">Email</label>
153
                                <input id="login" name="login" type="text" class="validate">
154
                                <label for="login">Login</label>
155
                            </div>
155
                            </div>
156
                            <div class="input-field col s12">
156
                            <div class="input-field col s12">
157
                                <input id="password" type="password" class="validate">
158
                                <label for="password">Password</label>
157
                                <input id="passphrase" name="passphrase" type="password" class="validate">
158
                                <label for="passphrase">Passphrase</label>
159
                            </div>
159
                            </div>
160
                            <div class="input-field col s12">
160
                            <div class="input-field col s12">
161
                                <button class="btn waves-effect waves-light" type="submit" name="action">
161
                                <button class="btn waves-effect waves-light" type="submit" name="action">

+ 174 - 73
html/workspace.html

1
{{ define "title" }}Loop Workspace{{ end }}
1
{{ define "title" }}Loop: Workspace{{ end }}
2
{{ define "styles" }}
2
{{ define "styles" }}
3
    <style>
3
    <style>
4
        pre#editor {
4
        pre#editor {
5
            height: 100vh;
5
            height: 100vh;
6
        }
6
        }
7
7
8
        iframe#view {
9
            height: 100vh;
10
            borders: none;
11
        }
12
13
        div#welcome {
14
            height: 100vh;
15
        }
16
17
        div#welcome h5 {
18
            width: 100%;
19
        }
20
8
        div#holder {
21
        div#holder {
9
            border: 2px dashed #ccc;
22
            border: 2px dashed #ccc;
10
            /*width: 300px;*/
23
            /*width: 300px;*/
11
            min-height: 110px;
24
            min-height: 110px;
12
            margin: 20px auto;
25
            margin: 20px auto;
13
        }
26
        }
27
28
        .select-wrapper {
29
            z-index: 2;
30
        }
31
14
        div#holder.hover {
32
        div#holder.hover {
15
            border: 2px dashed #0c0;
33
            border: 2px dashed #0c0;
16
        }
34
        }
35
17
        div#holder p {
36
        div#holder p {
18
            margin: 10px;
37
            margin: 10px;
19
            font-size: 14px;
38
            font-size: 14px;
20
        }
39
        }
40
21
        progress {
41
        progress {
22
            width: 100%;
42
            width: 100%;
23
        }
43
        }
44
24
        progress:after {
45
        progress:after {
25
            content: '%';
46
            content: '%';
26
        }
47
        }
48
27
        .fail {
49
        .fail {
28
            background: #c00;
50
            background: #c00;
29
            padding: 2px;
51
            padding: 2px;
30
            color: #fff;
52
            color: #fff;
31
        }
53
        }
54
32
        .hidden {
55
        .hidden {
33
            display: none !important;
56
            display: none !important;
34
        }
57
        }
58
59
        #editor-actions a.btn-flat {
60
            padding: 0 0.5rem;
61
        }
62
63
        .fixed-action-btn ul button.btn-floating {
64
            opacity: 0;
65
        }
66
67
        .fullscreen {
68
            width: 100%;
69
            height: 100vh;
70
        }
35
    </style>
71
    </style>
36
{{ end }}
72
{{ end }}
37
{{ define "content" }}
73
{{ define "content" }}
38
    <div id="toolbar" class="row">
39
        <div class="col s12 center">
40
            <!-- Toolbar of stuffs -->
41
            <button id="fullscreen" class="btn waves-effect waves-light">
42
                <i class="mdi-navigation-fullscreen left hide-on-med-and-down"></i>
43
                <i class="mdi-navigation-fullscreen hide-on-large-only"></i>
44
                <span class="hide-on-med-and-down">Focus Mode</span>
45
            </button>
46
            <button id="save" class="btn waves-effect waves-light">
47
                <i class="mdi-content-save left hide-on-med-and-down"></i>
48
                <i class="mdi-content-save hide-on-large-only"></i>
49
                <span class="hide-on-med-and-down">Save</span>
50
            </button>
51
            <button id="create" class="btn waves-effect waves-light">
52
                <i class="mdi-content-create left hide-on-med-and-down"></i>
53
                <i class="mdi-content-create hide-on-large-only"></i>
54
                <span class="hide-on-med-and-down">Create</span>
55
            </button>
56
            <button id="publish" class="btn waves-effect waves-light">
57
                <i class="mdi-editor-publish left hide-on-med-and-down"></i>
58
                <i class="mdi-editor-publish hide-on-large-only"></i>
59
                <span class="hide-on-med-and-down">Publish</span>
60
            </button>
61
            <a id="upload" class="btn waves-effect waves-light modal-trigger" href="#upload-area">
62
                <i class="mdi-file-file-upload left hide-on-med-and-down"></i>
63
                <i class="mdi-file-file-upload hide-on-large-only"></i>
64
                <span class="hide-on-med-and-down">Upload</span>
65
            </a>
66
        </div>
67
    </div>
68
    <div class="row">
74
    <div class="row">
69
        <div id="left-pane" class="col s4 m4 l2">
75
        <div id="left-pane" class="col s4 m4 l2">
70
            <!-- Directory of stuffs -->
71
            <div class="row">
76
            <div class="row">
72
                <div class="col s12">
77
                <div class="col s12">
73
                    <label>Workspace</label>
78
                    <label>Workspace</label>
74
                    <select>
79
                    <select>
75
                        <option value="" disabled>Select a directory</option>
80
                        <option value="" disabled>Select a directory</option>
76
                        <option value="Persona" selected>Personal</option>
77
                        <option value="Blog">Blog</option>
81
                        <option value="Personal" selected>Personal</option>
78
                    </select>
82
                    </select>
79
                </div>
83
                </div>
84
                <div class="col s12 center">
85
                    <a class="waves-effect waves-light btn modal-trigger" href="#modal-new-directory">New Content</a>
86
                </div>
80
            </div>
87
            </div>
81
            <div class="row">
88
            <div class="row">
82
                <div class="col s12">
89
                <div class="col s12" style="overflow-y: auto;">
83
                    <ul class="collection">
90
                    <ul class="collection">
84
                        <li class="collection-item avatar dismissable">
85
                            <i class="mdi-file-folder circle"></i>
86
                            <span class="title">Title</span>
87
                            <p>(3 files)</p>
88
                            <a href="#" class="secondary-content"><i class="mdi-action-grade"></i></a>
89
                        </li>
90
                        <li class="collection-item avatar dismissable">
91
                            <i class="mdi-file-folder circle"></i>
92
                            <span class="title">Title</span>
93
                            <p></p>
94
                            <a href="#" class="secondary-content"><i class="mdi-action-grade"></i></a>
95
                        </li>
96
                        <li class="collection-item avatar dismissable">
97
                            <i class="mdi-file-folder circle"></i>
98
                            <span class="title">Title</span>
99
                            <p></p>
100
                            <a href="#" class="secondary-content"><i class="mdi-action-grade"></i></a>
101
                        </li>
102
                        <li class="collection-item avatar dismissable">
103
                            <i class="mdi-file-folder circle"></i>
104
                            <span class="title">Title</span>
105
                            <p>(7 files)</p>
106
                            <a href="#" class="secondary-content"><i class="mdi-action-grade"></i></a>
107
                        </li>
91
                        {{ range $d := .Directories }}
92
                            <li class="collection-item avatar dismissable">
93
                                <i class="mdi-file-folder circle"></i>
94
                                <span class="title"><a href="/{{ $.Username }}/edit/{{ $d }}">{{ $d }}</a></span>
95
                                <p><!--(3 files)--></p>
96
                                <a href="#" class="secondary-content"><i class="mdi-action-grade"></i></a>
97
                            </li>
98
                        {{ end }}
108
                    </ul>
99
                    </ul>
109
                </div>
100
                </div>
110
            </div>
101
            </div>
111
        </div>
102
        </div>
112
        <div id="workarea" class="col s8 m8 l10">
103
        <div id="workarea" class="col s8 m8 l10">
113
            <div class="row">
114
                <pre id="editor" class="col s12 m12 l12"></pre>
115
                <!--<div id="preview" class="col s12 m12 l6"></div>-->
104
            <div class="fixed-action-btn" style="bottom: 45px; right: 24px;">
105
                <a id="create" class="btn-floating btn-large red" title="Edit">
106
                    <i class="large mdi-editor-mode-edit"></i>
107
                </a>
108
                <ul>
109
                    <li><a id="delete" class="btn-floating red" title="Delete"><i class="large mdi-action-delete"></i></a></li>
110
                    <li><a id="save" class="btn-floating amber" title="Save"><i class="large mdi-content-save"></i></a></li>
111
                    <li><a id="publish" class="btn-floating green" title="Publish"><i class="large mdi-editor-publish"></i></a></li>
112
                    <li><a id="upload" class="btn-floating blue modal-trigger" href="#upload-area" title="Attach file"><i class="large mdi-editor-attach-file"></i></a></li>
113
                </ul>
116
            </div>
114
            </div>
115
            {{ if .Title }}
116
                <div id="zoom" class="row">
117
                    <div id="editor-actions" class="col s12 m12 l12">
118
                        <a id="fullscreen" class="waves-effect waves-teal btn-flat right" title="Fullscreen">
119
                            <i class="mdi-navigation-fullscreen left hide-on-med-and-down"></i>
120
                            <i class="mdi-navigation-fullscreen hide-on-large-only"></i>
121
                            <span class="hide-on-med-and-down">Focus Mode</span>
122
                        </a>
123
                        <a id="preview" class="waves-effect waves-teal btn-flat right" title="Toggle Mode">
124
                            <i class="mdi-action-cached left hide-on-med-and-down"></i>
125
                            <i class="mdi-action-cached hide-on-large-only"></i>
126
                            <span class="hide-on-med-and-down">Preview</span>
127
                        </a>
128
                    </div>
129
                    <div class="col s12 m12 l12"><h4>{{ .Title }}</h4></div>
130
                    <pre id="editor" class="col s12 m12 l12">{{ .Content }}</pre>
131
                    <iframe id="view" class="col s12 m12 l12" style="display: none;" frameBorder="0" seamless="seamless" src="/{{ .Username }}/edit/{{ .Title }}/preview"></iframe>
132
                </div>
133
            {{ else }}
134
                <div class="row">
135
                    <div id="welcome" class="col s12 m12 l12 valign-wrapper">
136
                        <h5 class="valign center">Welcome!</h5>
137
                    </div>
138
                </div>
139
            {{ end }}
117
        </div>
140
        </div>
118
    </div>
141
    </div>
119
    <div class="row">
142
    <div class="row">
121
            <div id="upload-area" class="modal bottom-sheet">
144
            <div id="upload-area" class="modal bottom-sheet">
122
                <div class="modal-content">
145
                <div class="modal-content">
123
                    <h4>Upload</h4>
146
                    <h4>Upload</h4>
124
125
                    <div id="holder" class="valign-wrapper">
147
                    <div id="holder" class="valign-wrapper">
126
                        <p class="valign center" style="width: 100%;">Drag &amp; drop your files here!</p>
148
                        <p class="valign center" style="width: 100%;">Drag &amp; drop your files here!</p>
127
                    </div>
149
                    </div>
139
            </div>
161
            </div>
140
        </div>
162
        </div>
141
    </div>
163
    </div>
164
    <div id="modal-new-directory" class="modal">
165
        <div class="modal-content">
166
            <h4>New Content</h4>
167
            <form id="new-directory" class="col s12" method="post" action="/{{ .Username }}/edit/">
168
                <div class="row">
169
                    <div class="input-field col s12">
170
                        <input id="title" name="title" type="text" class="validate">
171
                        <label for="title">Document Title</label>
172
                    </div>
173
                </div>
174
            </form>
175
        </div>
176
        <div class="modal-footer">
177
            <a id="new" href="#" class="modal-action modal-close waves-effect waves-green btn-flat">Create</a>
178
            <a href="#" class="modal-action modal-close waves-effect waves-red btn-flat">Cancel</a>
179
        </div>
180
    </div>
142
{{ end }}
181
{{ end }}
143
{{ define "scripts" }}
182
{{ define "scripts" }}
144
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.1.8/ace.js"></script>
183
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.1.8/ace.js"></script>
146
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.1.8/mode-markdown.js"></script>
185
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.1.8/mode-markdown.js"></script>
147
    <script type="text/javascript">
186
    <script type="text/javascript">
148
        (function () {
187
        (function () {
149
            var editor = ace.edit('editor');
150
            editor.setTheme('ace/theme/chrome');
151
            editor.getSession().setMode('ace/mode/markdown');
188
            if (document.getElementById('editor')) {
189
                var editor = ace.edit('editor');
190
                editor.setTheme('ace/theme/chrome');
191
                editor.getSession().setMode('ace/mode/markdown');
192
            }
152
        })();
193
        })();
153
194
154
        (function () {
195
        (function () {
232
273
233
        $(function () {
274
        $(function () {
234
            function toggleFullScreen() {
275
            function toggleFullScreen() {
235
                var editorElement = document.getElementById('editor');
276
                var zoom = document.getElementById('zoom');
236
277
237
                if (!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullScreenElement && !document.msFullscreenElement) {
278
                if (!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) {
238
                    if (document.documentElement.requestFullscreen) {
279
                    if (document.documentElement.requestFullscreen) {
239
                        editorElement.requestFullscreen();
280
                        zoom.requestFullscreen();
240
                    } else if (document.documentElement.msRequestFullscreen) {
281
                    } else if (document.documentElement.msRequestFullscreen) {
241
                        editorElement.msRequestFullscreen();
282
                        zoom.msRequestFullscreen();
242
                    } else if (document.documentElement.mozRequestFullScreen) {
283
                    } else if (document.documentElement.mozRequestFullScreen) {
243
                        editorElement.mozRequestFullScreen();
284
                        zoom.mozRequestFullScreen();
244
                    } else if (document.documentElement.webkitRequestFullscreen) {
285
                    } else if (document.documentElement.webkitRequestFullscreen) {
245
                        editorElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
286
                        zoom.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
246
                    }
287
                    }
247
                } else {
288
                } else {
248
                    if (document.exitFullscreen) {
289
                    if (document.exitFullscreen) {
259
300
260
            $('select').material_select();
301
            $('select').material_select();
261
            $('.modal-trigger').leanModal();
302
            $('.modal-trigger').leanModal();
262
            $('button#fullscreen').click(function (event) {
303
304
            $('#fullscreen').click(function (event) {
263
                event.preventDefault();
305
                event.preventDefault();
306
                $('#zoom').toggleClass('fullscreen');
264
                toggleFullScreen();
307
                toggleFullScreen();
265
            });
308
            });
309
310
            $('#preview').click(function (event) {
311
                event.preventDefault();
312
                var editor = ace.edit('editor');
313
314
                if (!$('#view').is(':visible')) {
315
                    $.ajax({
316
                        url: window.location.href,
317
                        method: 'put',
318
                        data: {
319
                            'contents': editor.getValue()
320
                        }
321
                    })
322
                    .done(function () {
323
                        $('#view').attr('src', $('#view').attr('src'));
324
                        $('#editor, #view').toggle();
325
                    });
326
                } else {
327
                    $('#editor, #view').toggle();
328
                }
329
            });
330
331
            $('a#delete').click(function (event) {
332
                event.preventDefault();
333
                var title = $('input#title').val();
334
335
                $.ajax({
336
                    url: window.location.href,
337
                    method: 'delete'
338
                })
339
                .done(function () {
340
                    window.location.href = '/{{ .Username }}';
341
                });
342
            });
343
344
            $('a#new').click(function (event) {
345
                event.preventDefault();
346
                var title = $('input#title').val();
347
348
                var form = $('form#new-directory');
349
                form.attr('action', form.attr('action') + title);
350
                form.submit();
351
352
                return false;
353
            });
354
355
            $('a#save').click(function (event) {
356
                event.preventDefault();
357
                var editor = ace.edit('editor');
358
359
                $.ajax({
360
                    url: window.location.href,
361
                    method: 'put',
362
                    data: {
363
                        'contents': editor.getValue()
364
                    }
365
                });
366
            });
266
        });
367
        });
267
    </script>
368
    </script>
268
{{ end }}
369
{{ end }}

BIN
loop


+ 11 - 3
main.go

21
	auth := r.PathPrefix("/logon").Subrouter()
21
	auth := r.PathPrefix("/logon").Subrouter()
22
	auth.Methods("POST").HandlerFunc(handlers.LogonHandler)
22
	auth.Methods("POST").HandlerFunc(handlers.LogonHandler)
23
23
24
	content := r.PathPrefix("/{username}").Subrouter()
24
	doc := r.PathPrefix("/{user}/edit/{title}").Subrouter()
25
	doc.HandleFunc("/preview", handlers.ContentPreviewHandler).Methods("GET")
26
	doc.Methods("GET").HandlerFunc(handlers.ContentReadHandler)
27
	doc.Methods("POST").HandlerFunc(handlers.ContentCreateHandler)
28
	//doc.Methods("POST").HandlerFunc(handlers.ContentUploadHandler)
29
	doc.Methods("PUT").HandlerFunc(handlers.ContentUpdateHandler)
30
	doc.Methods("DELETE").HandlerFunc(handlers.ContentDeleteHandler)
31
32
	content := r.PathPrefix("/{user}").Subrouter()
25
	content.Methods("GET").HandlerFunc(handlers.ContentHandler)
33
	content.Methods("GET").HandlerFunc(handlers.ContentHandler)
26
34
27
	article := r.PathPrefix("/{username}/blog/{article}").Subrouter()
35
	article := r.PathPrefix("/{user}/blog/{article}").Subrouter()
28
	article.Methods("GET").HandlerFunc(handlers.BlogArticleHandler)
36
	article.Methods("GET").HandlerFunc(handlers.BlogArticleHandler)
29
37
30
	blog := r.PathPrefix("/{username}/blog").Subrouter()
38
	blog := r.PathPrefix("/{user}/blog").Subrouter()
31
	blog.Methods("GET").HandlerFunc(handlers.BlogHandler)
39
	blog.Methods("GET").HandlerFunc(handlers.BlogHandler)
32
40
33
	log.Print("Serving content on ", environment.Address())
41
	log.Print("Serving content on ", environment.Address())

+ 9 - 4
security/security_test.go

1
package security
1
package security
2
2
3
import (
4
	"testing"
5
)
3
import "testing"
6
4
7
func TestEncoding(t *testing.T) {
5
func TestEncoding(t *testing.T) {
8
	clear := "encoding test"
6
	clear := "encoding test"
40
}
38
}
41
39
42
func TestGeneratePassphrase(t *testing.T) {
40
func TestGeneratePassphrase(t *testing.T) {
43
	t.Skip("Need a test for generating a passphrase")
41
	username := "bryan"
42
	passphrase := "test"
43
	expected := "o47ZaAu3pd5cK0ZA412Z3rpsi8MDdlUZPkJ9Z51x7QlNUc7S4EGYuT9XIe4HYtzy7vxHah528ibkfd4CjHCSsg=="
44
	generated := GeneratePassphrase(username, passphrase)
45
46
	if expected != generated {
47
		t.FailNow()
48
	}
44
}
49
}